今年も Advent Calendar で記事を1つこさえました。
今年を振り返ると、なんか変なことを色々やってたなぁ、という気持ちです。
社で必要になったので、Go 言語の OSS を書きました。
この OSS がなんなのかは スライド を見ていただくとして、テストをめっちゃ書いたのでだんだんどうテストを書いたらやりやすいのかがわかってきました。この成果自体はいつか vanilla-manifesto へまとめると思いますが、簡単に説明しようと思います。
書いてて辛くないテストをするために、もっとも重要なのは テスト対象の設計 です。設計といえば、よく SOLID がよいオブジェクト指向設計の指針とされますが、テストの文脈でも同じことがあてはまります。例えば、テスト対象が SOLID をよく守っていると次のように嬉しいことがあります:
ということで「テストしやすい工夫」の実態とは「テストしやすいテスト対象の設計の工夫」と同じ意味なので、この記事で紹介するのは後者がメインになります。
さて、今回紹介するプラクティスを大雑把にいうと「だいたいの処理を関数を受け取る関数でさばく」という過激なものです。具体的手法はあとで説明しますが、これをすると、単一責任原則・開放閉鎖原則・インターフェース分離の原則・依存性逆転の原則がかなり高まり、テストの書きやすさがとてもあがります。
簡単に例を見てみましょう。Hoge という Web API があったとして、以下の 2 つを提供しているとします:
これを過激派のアプローチで実装すると、次のようになります:
type HttpDoer func(*http.Request) (*http.Response, error) type Hoge string // Hoge を Get する関数のインターフェースのようなもの。 type HogeGetter func() (*Hoge, error) // Hoge を Get する具体的な関数のコンストラクタのようなもの。 func CreateHogeGetter(httpDo HttpDoer) HogeGetter { return func() (*Hoge, error) { request, err := http.NewRequest("GET", "http://example.com/hoge", nil) if err != nil { return nil, err } response, err := httpDo(request) if err != nil { return nil, err } if response.StatusCode < 200 || 300 <= response.StatusCode { return nil, fmt.Errorf("unsuccessful http response: %s", response.Status) } bs, err := ioutil.ReadAll(response.Body) if err != nil { return nil, err } hoge := Hoge(bs) return &hoge, nil } }
あっ、待って!ちょっと待って!まだ読むのをやめないで!
ここで、なぜ http.Client
を直接使わずに HttpDoer
という関数を作っているかというと、 次のようにテストがめちゃ簡単になるからです:
func TestHogeGetter(t *testing.T) { response := AnyHttpResponse() response.StatusCode = 200 response.Body = ioutil.NopCloser(strings.NewReader("HOGE")) httpDo := ConstHttpDoerStub(response, nil) getHoge := CreateHogeGetter(httpDo) actual, err := getHoge() if err != nil { t.Errorf("want (_, nil), got (_, %v)", err) return } expected := Hoge("HOGE") if *actual != expected { t.Errorf("want (%v, nil), got (%v, nil)", expected, *actual) return } }
ここでは、AnyHttpResponse
と HttpDoerStub
という 2 つのテストダブルファクトリが登場します。それぞれの定義は次のようになっています:
// 指定したレスポンスとエラーを常に返す偽物。 func ConstHttpDoerStub(response *http.Response, err error) HttpDoer { return func(*http.Request) (*http.Response, error) { return response, err } } // よく使うテストダブルは名前をつけて定義しておくと便利。 // これは内容はともかく成功したっぽいレスポンスになるので、通信の成功だけをみているテスト対象が多ければよく使うことになる。 func AnySuccessfulHttpDoerStub() HttpDoer { response := AnyHttpResponse() response.StatusCode = 200 return ConstHttpDoerStub(response, nil) } // 適当なレスポンスを作成する。 func AnyHttpResponse() *http.Response { // テスト側で必要な値だけを指せるようにすると、テストがどこに着目しているのかがわかりやすくなる。 // ありえない値を指定しているのは、テスト対象に影響を与えているのにテスト側で指定し忘れるミスに気付きやすくするため。 return &http.Response{ Status: "000 Dummy Response", StatusCode: 0, Proto: "HTTP/0.0", ProtoMajor: 0, ProtoMinor: 0, Header: nil, Body: nil, ContentLength: 0, TransferEncoding: nil, Close: false, Uncompressed: false, Trailer: nil, Request: nil, TLS: nil, } }
生産性の高いテストを書く上では、(1) 間違えにくく、(2) 再利用しやすい、の 2 条件を満たすテストダブルがとても重要です。この点、上のテストダブルは極めて単純なため間違った使い方をしづらく、さらに HTTP 通信を必要とするどのテストでも再利用できます。例えば、Hoge API の POST 側のテストを次のようにできます:
func TestPost(t *testing.T) { response := AnyHttpResponse() // 再利用 response.StatusCode = 201 response.Body = ioutil.NopCloser(strings.NewReader("OK")) httpDo := ConstHttpDoerStub(response, nil) // 再利用 postHoge := CreateHogePoster(httpDo) hoge := Hoge("") err := postHoge(&hoge) if err != nil { t.Errorf("want (_, nil), got (_, %v)", err) return } }
うまく再利用できている様子がわかります。
また、このようなテストダブルファクトリを用意することで、テスト側でどのような HTTP レスポンスが帰ってくることを期待しているのかが極めて明確になっています:
func TestPost(t *testing.T) { response := AnyHttpResponse() response.StatusCode = 201 response.Body = ioutil.NopCloser(strings.NewReader("OK")) httpDo := ConstHttpDoerStub(response, nil) // ステータスコード 201 で内容が OK なレスポンスを期待していることがすぐ読み取れる
さらに、テストダブルが関数なため柔軟性も高く、例えばリクエストの内容に応じてレスポンスを変えたくなったら次のように書けます:
func TestHogeGetter(t *testing.T) { httpDo := func(request *http.Request) (*http.Response, error) { response := AnyHttpResponse() if strings.HasSuffix(request.URL.Path, "hoge") { response.StatusCode = 200 response.Body = ioutil.NopCloser(strings.NewReader(request.URL.Path)) } else { response.StatusCode = 400 response.Body = ioutil.NopCloser(strings.NewReader("")) } return response, nil } getHoge := CreateHogeGetter(httpDo)
どのように動くのかが一目瞭然です!(モックライブラリだとこうはいかない)
また、これにはもう一つ利点があります。上のテストダブルはそこそこ複雑なので「書きたくないなぁ」という嫌な印象をうけないでしょうか? この「書きたくないなぁ」という感覚はとても重要で、このとき偽装対象に期待する振る舞いが多い(=密結合)ことを示唆しています。つまり、偽装対象との間に抽象層を挟むなり、偽装対象を分割するなりのきっかけとできるわけです。
なお、これまではテスト面だけのメリットを強調してきましたが、やってきたことは SOLID を意識するということだったのでテスト対象側にもうれしさがあります。例えば、HTTP 通信の結果をキャッシュしたいと思ったとすると、既存の定義に影響を与えずに(=既存テストを書き換えることなく)キャッシュするバージョンをすぐに作れます:
func CreateCachedHttpDoer(httpDo HttpDoer) HttpDoer { cache := make(map[string]*ClonableResponse) return func(request *http.Request) (*http.Response, error) { if clonable, ok := cache[request.URL.String()]; ok { return clonable.Clone(), nil } response, err := httpDo(request) if err != nil { return nil, err } clonable := ClonableResponse(response) cache[request.URL.String()] = clonable return clonable.Clone(), nil } }
さて、もちろん良さの裏側には悪さもあります。欠点も紹介しておきます。
この種のパターンをかなり厳密に適用すると、依存を解決する部分がモリモリになります。慣れれば型を見れば何を要求されているのかはすぐにわかって苦にならなくなるのですが、コードの見た目はかなり悪いです。
あと、関数の型定義がめちゃ増えるのですが、命名に困るようになります。上の OSS でも結構苦しかった記憶が…
Go でテストしやすい環境を作るには、とにかく関数を使いまくってテストダブルを再利用するとよかったという話でした。
これまで説明してきたことは、DeNA/devfarm の executor まわり で実用しているので、もし気なる場合は参考にしてみてもいいかもしれません。テストダブルは *_stub.go
という命名規則で分離しているので、すぐに見つけられると思います。
ブログタイトルを変えました。前のタイトルは「若き何かの悩み」で、さらにその前のタイトルは「若き JavaScripter の悩み」でした。
若い頃は、JavaScript をやっていました(若き JavaScripter の悩み期)。
これだとマッチする企業が少なすぎて、手を広げることにしました。この時もまだ若くて、ただ JS から離れて iOS とか Ansible とか Jenkins とかをやっていたので、もう JavaScripter ではないなになってタイトルを変えました(若き何かの悩み期)。
今はもう若くないのでタイトルを変えました(若くない何かの悩み期)。最近やっているのは、形式手法 / ゲームテスト支援(Go とか) / バグ分析基盤構築 / にわかスクラムマスター / 闇業(メール書いて待って書いて待つ)です。
今後ともよろしくお願いします。
あと、 @cocopon から祝いをもらいました。
@cocopon より、若くなくなった祝いをいただきました。クオリティ高え、、、、大切に使います https://t.co/mjNx9yPLUV pic.twitter.com/hp9RgnP1c1
— クニワッ (@orga_chem) November 8, 2019
学生時代最後の春休み(2014年)につくって reject されていた LINE スタンプが、なんと4年後の今日登録されました(再申請は今年2月に依頼しています)。
なお、これらの画像群はもともとは LINE のスタンプ 向けに作成したものです。ただ、初回申請時に微妙な条項で reject されてお蔵入りになりかけたのですが、もったいないので積極的にスライドに使って減価償却を続けていました。最近でもときどき画像を追加しています。
もともとは、私が小学生ぐらいのときに書いたキャラクターです。実は最近のは目が閉じているバージョンで、当時は目が開いていました。 なお、@cocopon と共同で開発していたゲームにボスキャラとして出現しますので、これをプレイすると目が開いているバージョンを見られます。
こちらが伝説のハートボール開眼シーンです。公開からもう10年以上経ってるとか信じられない…!(信じたくない) pic.twitter.com/dDIdorcstp
— cocopon (@cocopon) July 9, 2019
フリーでオープンソースなエディタである Inkscape を使っています。
未定義です。処理系にもよりますが、出っ張っていることがあります。
近年、オフィスや休憩スペースなどで落ち着けない問題があります。落ち着き環境は生産性に大きなインパクトがありますね。 さて、いろいろグッズを集めたところ落ち着き環境の構築に成功したので共有します。
特におすすめなのは以下の2つです:
AVANTEK 防音イヤーマフ 遮音値34dB 金属なし 耐摩素材 超弾力性ヘッドバンド ANSI S3.19&CE EN352-1認証済み 聴覚保護 (ブルー)
HUYOU(ふよう) 立体型 アイマスク 耳栓セット 収納袋付 (ブラック)
以降は、詳細や他に試した製品との比較です。
続きを読む