エラーを報告する際に意識していること
基本的には質問をせずに粘ってしまうタイプなのだが、環境構築の段階でのエラーや、人生で初見のエラーなどは経験上、粘っても何も得られないので、即質問するようにしている。
そこで意識していることは、
- 何が原因で何ができなかったのかを記す
- 自分が打ったコマンドのログを全て記す
- なぜそのコマンドを打ったのかの情報源を記す
- その結果のログを記す
- 自分なりの解決案があれば記す
具体例
github.com
qrcode.react とスナップショットテストの相性が最高すぎた
qrcode.react
は、その名の通りQRコードを生成してくれるReactコンポーネントだ。
github.com
ファイル構成はsrc/index.js
でゴリゴリ計算というか文字列を算出し、コンポーネントを返す構成だった。
テストがなかったので、テストを追加したかったのだが、何に対してテストを追加すればよいのかわからず、しばらく放置していた。
convertStr
関数やgeneratePath
関数の入力と出力に対する単体テストを追加しようと思ったけれど、あまり恩恵はないように思えた。
qrcode.react/index.js at 6aeb42abc26ffecc868b630b6ad8f507d2125813 · zpao/qrcode.react · GitHub
qrcode.react/index.js at 6aeb42abc26ffecc868b630b6ad8f507d2125813 · zpao/qrcode.react · GitHub
そんなとき、以下の記事を目にした。
www.mizdra.net
試しにqrcode.react
で試してみると、とてもよい結果が得られた。
何がよかったかと思うと、convertStr
関数やgeneratePath
関数の入力と出力に対する単体テストをせずとも、それらがうまく機能していることを保証でき、なにより、path
コンポーネントのd
の値の複雑性がスナップショットの出力により明らかになり、スナップショットの結果が1つの情報としても機能しているところだ。
<path d="M0,0 h29v29H0z" fill="#ffffff" /> <path d="M0 0h7v1H0zM8 0h1v1H8zM10 0h5v1H10zM17 0h2v1H17zM22,0 h7v1H22zM0 1h1v1H0zM6 1h1v1H6zM9 1h2v1H9zM13 1h1v1H13zM19 .... fill="#000000" />
マージされなくてもブログを書けば供養できると思って書いた。
npmから@typesを使う場合はDefinitelyTypedを見に行かないといけないことがある
Link Preview JS
という、「allows you to extract information from a HTTP url/link (or parse a HTML document) and retrieve meta information such as title, description, images, videos, etc」なレポジトリがあった。
そこでは、cheerioというライブラリを用いて、パースされたレスポンスから特定の要素を取得している。
https://github.com/ospfranco/link-preview-js/blob/d22888f26718674d7a32cfb935434f7357c80817/index.ts#L236
Link Preview JS
はTypeScriptで書かれているのだが、cheerio
に対する型が全くなく、any
が使用されていたので、@types/cheerioを導入した。
だが、一部だけ以下のようなエラーがでて、@types/cheerio
を用いてもany
が残ってしまっていた。
Property 'attribs' does not exist on type 'Element'. Property 'attribs' does not exist on type 'TextElement'. ts(2339)
https://github.com/ospfranco/link-preview-js/pull/83#discussion_r563268635
このまま放置するのも気持ちが悪かったので、実際に@types/cherrio
の型が書かれているDefinitely Typed
をみにいった。
github.com
DefinitelyTyped/types/cheerio at master · DefinitelyTyped/DefinitelyTyped · GitHub
すると、cheerio.Element
のTagElement
にのみattribs
の型が定義されていることがわかった。
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/68e48c5ad46f0e7733feace0a9120d7dc3108462/types/cheerio/index.d.ts#L33
テストを見ても、element
のtype
がtext
ではない、つまり、TextElement
ではなくTagElement
であることを確認している。
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b41bbafcd298b4b2aaa851d43ffadb6ef3565e69/types/cheerio/cheerio-tests.ts#L347
なので、それを適用して、自分で作ったany
を回収しにいった。
github.com
src/@types/*d.ts
に型定義があるのと比べて、npmでインストールした場合は、node_modules/@types/*/*d.ts
に型定義が隠蔽されてしまうので、便利だけど難しいなと思う。
それを解消する何かがもうすでにあるのだろうか。
TwitterでこのOSSの作者から感謝してもらえた。
https://twitter.com/ospfranco/status/1353312260823855105
(フォローしたけどフォローは返ってこなかった)
自分のPRを見て思う、TableDrivenTestsの良さ
jd
という、commandline utility and Go library for diffing and patching JSON values
なライブラリにTableDrivenTestsを導入したので、その良さを主観的にまとめておこうと思う。
まずは、実際にdiffの一部を見てみる。
https://github.com/josephburnett/jd/pull/29/files#diff-a0a78ab7b3426f60e7b758b565129777bdcef35e95a490999a0ff377389576d1R83
before
ctx := newTestContext(t) checkDiff(ctx, `[]`, `[]`) checkDiff(ctx, `[[]]`, `[[1]]`, `@ [0,0]`, `+ 1`) checkDiff(ctx, `[1,2,3]`, `[1,null,3]`, `@ [1]`, `- 2`, `+ null`) checkDiff(ctx, `[]`, `[3,4,5]`, `@ [0]`, `+ 3`, `@ [1]`, `+ 4`, `@ [2]`, `+ 5`)
after
ctx := newTestContext(t) tests := []struct { context *testContext a string b string diffLines []string }{ {ctx, `[]`, `[]`, []string {}}, {ctx, `[[]]`, `[[1]]`, []string { `@ [0,0]`, `+ 1`, }, }, {ctx, `[1,2,3]`, `[1,null,3]`, []string { `@ [1]`, `- 2`, `+ null`, }, }, {ctx, `[]`, `[3,4,5]`, []string { `@ [0]`, `+ 3`, `@ [1]`, `+ 4`, `@ [2]`, `+ 5`, }, }, } for _, tt := range tests { checkDiff(tt.context, tt.a, tt.b, tt.diffLines...) }
よいと思うところを2つ書く。
関数呼び出しが冗長でなくなった
beforeとafterで関数を呼び出す回数は変わらないが、afterはループの中で呼び出しているので、パッと見て冗長な表現にはなっていない。
func TestArrayDiff(t *testing.T) {
のなかのテストなので、checkDiff
を何回も呼び出している(ように見える)のは多少くどい。
関数の引数が宣言的になった
checkDiff
関数は、func checkDiff(ctx *testContext, a, b string, diffLines ...string) {
という形で引数を取る。
jd/common_test.go at d1d4edfee5375f88c1155ef55158597fb8488751 · josephburnett/jd · GitHub
beforeでは、どこからがdiffLines
が多少わかりにくくなっているが、afterでは、[]string
でまとめているため、checkDiff
に何がどの引数で渡されるのかが分かりやすくなっている。
{ context *testContext a string b string diffLines []string }
また、上のように、テストのはじめに宣言できることで、可読性が非常に高くなっている。
今回のようにbeforeがあの状態だったら、TableDrivenTestを導入しない手はないのではないかと思う。
割といい変更がだせたので自画自賛タイトルをつけてみた。
単純作業の効用は意外と大きい
owという、TypeScriptの型制限を拡張したOSSに何度かコミットして気づいたことは、単純作業は意外と効用が大きいということだ。
github.com
具体的に何をしたかというと、typescript-eslint
の、explicit-function-return-typeが無効になっていたのを、有効にした。
github.com
まずはじめにpackage.json
のexplicit-function-return-type
の設定を"off"
から"error"
に変更し、テストを走らせた。
https://github.com/sindresorhus/ow/pull/206/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R89
あとは、そのテストが落ちた箇所にひたすら型をつけていった。
この型づけは自分的には単純作業のうちに入るのだが、自分のスキルはそれほど伸びないものの、得られる効用は大きい。
ほんの30分の型づけにより、これからこのOSSにコミットする全ての人は、function return type をつけなければならなくなった(つけないとテストが落ちる)。
これは、このOSSの保守性に直結することである。
関数が返す型が明確になると、初めてこのOSSに訪れる人のコードリーディングが捗るというオプションもついてくる。
このように、O(1)の単純作業でO(N)の効用が得られる。
ただし、個人的にはこのexplicit-function-return-type
を有効にするには然るべきタイミングがあると思っている。
まず、プロジェクトの初期からこれを有効にすると、テストが高確率で落ちることになり、開発スピードが落ちてしまう。
また、explicit-function-return-type
を有効にするときに、他のPRが溜まっている状態だったら、凄まじいコンフリクトが生じてしまう。
https://github.com/sindresorhus/ow/commits/main
なので、今回は、OSSとしてある程度枯れていること、他の人とPRが被らない時期であることを確認してから、このPRを出した。
ちなみにこういった単純作業をするときは、ミスチルの彩りを聴きながらすると捗る。
Cypress で `blitz new` をテストしたい(が、できていない)
ことの発端は、最近よくBlitzを聞くようになり、GitHubを訪れると、good first issueが転がっていて、テストに関するものがあったので、Blitzを触ったことも、GraphQLやPrismaもあまり知らなかったけど、拾ってみた。
このissueの内容は、blitz new
の挙動をCypressを使って確かめたい、というものだった。
Add e2e CLI test for `blitz new` · Issue #37 · blitz-js/legacy-framework · GitHub
We currently have a few blitz new tests, but not a complete e2e test for the new app.
...
Add Cypress to our CLI testing setup. See the Redwood CLI cypress test for a great example/starting point.
Cypress
もはじめて聞く名前だったが、ドキュメントを読んでちょっと手元で動かして何となく理解し、Blitz
に導入することにした。
(Cypress
導入第1人者かと思っていたが、先人がいた)
マージされてコントリビューターにもなれたので、本腰をいれてテストを増やしていこうと思ったのだが、Blitz
レポジトリの中でblitz new
をe2eテストする、ということがよくわからなくなってしまった。
blitz
を試すために手元でblitz new
したレポジトリも作って触っていて、どっちで何をしたいのかわからなくなり混乱が加速した。
なので、issueのなかで言及されていた、Redwoodのe2eテストがどうなっているのかを見にいくことにした。
まずは、RedwoodのCONTRIBUTING.mdを読み、e2eテストを動かすことにした。
が、うまく動かなかったのでissueを出した。
すると、2日足らずで問題は解決したようで、Cypressに慣れるためにも新たにテストを追加した。
Cypress
は完全に理解できたので、Blitz
にも入れていきたいが、Blitz
レポジトリの中でblitz new
をe2eテストするということがよくわからい問題が健在している。
Redwood
では、起点がシェルスクリプトで、"We will copy './packages/create-redwood-app/template' and the packages/*"
していることがわかっている。
redwood/test-tutorial at d61734296bf843d50933d36cabb2a9ea25ede919 · redwoodjs/redwood · GitHub
だが、Blitz
に'./packages/create-redwood-app/template'
に当たるものがあるとは思えない。
また、これと同じようにシェルスクリプトで起動するとしても、Blitz
の場合はデフォルトのフォームを選択する部分があり、そこをどうモックしたらよいのかわからない問題もある。
blitz/new.ts at e2383fbb5f52a59042640dcf4cb67d2c8344de06 · blitz-js/blitz · GitHub
Blitz
既存のnew.test.ts
を見にいったところ、mockImplementationでモックしていることがわかったが、これはあくまでcliのテストなのでやりたいこととは違う。
jest.mock("enquirer", () => { return jest.fn().mockImplementation(() => { return { prompt: jest.fn().mockImplementation(() => ({form: "React Final Form"})), } }) })
詰まってしまったので、そもそもblitz new
コマンドを打ったら、どのようにしてファイル群を生成しているのかを見に行ったら、ヒントがあるのではないかと思い読み始めた。
すると、new.ts
では、generator
を生成してそれを走らせているっぽいことがわかった。
blitz/new.ts at e2383fbb5f52a59042640dcf4cb67d2c8344de06 · blitz-js/blitz · GitHub
blitz/new.ts at e2383fbb5f52a59042640dcf4cb67d2c8344de06 · blitz-js/blitz · GitHub
const generator = new (require("@blitzjs/generator").AppGenerator)({ ... await generator.run()
blitz new
のテストは実質的にはgenerator
のテストなのか?、ということで"@blitzjs/generator"
を読みにいこうとしたところで力尽きている。
exmaple
にすでにテストがあるのでそれでいいのではないかと思い始めているけど、勉強になりそうなのでちょっとづつ読んでいきたいと思っている。
https://github.com/blitz-js/blitz/blob/canary/examples/auth/cypress/integration/index.test.ts
2/19追記
was this issue resolved in #1846?
という質問が来ていたので、疑問点をissueに書き込んでおいた。
https://github.com/blitz-js/blitz/issues/1767#issuecomment-781707012
NTT Performance Tuning Contest の対策と反省と抱負
今までISUCONのようなパフォーマンスチューニングのイベントには参加したことがなかったのですが、興味はありました。
23卒があつまっているSlackで参加したいとつぶやくと、一瞬で反応が返ってきてチームを組めることになり、@PANORAMA_01さんと@marin_a___さんで参加しました。
大学ではHello World
しないのでこういうコミュニティありがたいです。
対策
言語はRubyを選んでいたので、Sinatra でくるだろうと思い、チュートリアルを軽く眺めてget
, post
の挙動を確かめたりしました。
Rubyの文法を忘れている部分があったのでちょっと復習しました。
qiita.com
パフォーマンスチューニングの雰囲気を掴むために過去開催されていたブログを読みました。
www.ntt.com
isucon.net
N+1問題について調べました。
Nは発行したクエリの数であり、探索したレコードの数ではないことを知り理解がクリアになりました。
qiita.com
qiita.com
インデックスとEXPLAINコマンドについて復習しました。
dev.mysql.com
qiita.com
blog.utgw.net
Nginxを調べて、リバースプロキシやキャッシュの設定を試しました。
proxy · wafuwafu13/sinatra-tutarial@fc25c4e · GitHub
画像キャッシュの設定 · wafuwafu13/sinatra-tutarial@8218b11 · GitHub
qiita.com
rin-ka.net
alpについて調べて試しました。
logをマウント · wafuwafu13/sinatra-tutarial@f49103e · GitHub
MySQLのスロークエリの設定やpt-query-digestについて調べて試しました。
pt-query-digest · Issue #1 · wafuwafu13/sinatra-tutarial · GitHub
masayuki14.hatenablog.com
thinkit.co.jp
dtyler.io
ISUCONのススメが前日に発売開始されていたので眺めました。
nextpublishing.jp
本番に向けて3人で勉強の進捗確認とか作戦会議をしました。
3人ともフロント寄りというかアプリケーションのコードを書いているのでゆるくがんばろうというモチベーションで参加しました。
反省と抱負
本番は、pt-query-digest入れて実行してスロークエリ見つけてインデックスをはってN+1を見つけて解決策がわからず放置してalpつかおうと思ってNginx入れようとしてわからなくなって放置して画像カラムどうにかしようとして解決策がわからなくなって放置して終了しました。
ベストプラクティスを脳に詰め込んでいっても手は動かないことを実感した7時間でした。
個々の使い方やボトルネックは分かるけど本番のアプリケーションにうまく落とし込めないのが放置して終了の原因だと思われます。
個々のアルゴリズムはわかるけど問題が解けない競プロと同じ感じがしています。
自分たちで本番と同じような環境を構築して経験を積んでコンテストに出ないと足元にもおよばないので練習あるのみという感じです。
ただ、今回参加に向けて勉強したり本番でできたりできなかったりした知見は結構大きいなと思っています。
あとは言語ですね、今回はRubyを使用しましたがパフォチューにはGoという感じがしているのでつめていきたいです。
言語やその周辺ライブラリの習得には、文法を理解する、自分でアプリケーションを作る、現場で使う、OSSを読んでPRする、の4段階あると思っています。
JS/TSは、現場で使う、OSSで生かす、OSSで学ぶ、現場で生かすのブーストが(たぶん)かかっている状態なので半年〜1年間でGoやその周辺ライブラリもそこまで持っていきたいと思います。
NTTさん、チームのかた、ありがとうございました!!
リベンジしたい!!!