テストを書きやすいコードを書く為に
前提
自分がアプリケーションを書く上で1番重要にしていることは、何と言ってもテストです。
テストが抜けてるコードをリファクタリングするなんて恐ろしい事はできないし、保守性の高いシステムを作る上で、多少冗長になったとしてもテストをきちんと書く事はとても大事だと思います。
そこで、カンムアドベントカレンダー9日目の記事として、普段テストを書きやすいコードを書く上で気をつけていることをいくつか言語化しておこうと思います。言語はGoです。
1. 多少冗長でも関数やメソッドはユースケースごとに分ける
複雑な関数を定義しないのは基本的な事ですが、処理が似ているコードを再利用しようとするあまり、1つの関数に処理を詰め込んでしまう事はよくある事だと思います。
例えば、ある関数AとTestAを用意したとします。
func A() string {
return "a"
}
func TestA(t *testing.T) {
assert.Equal(t, "a", A())
}
そこに仕様変更に伴い、後からIsSomeUsecase()という条件分岐を関数A追加した場合、TestAの中身も同様に条件分岐を書かないといけません。
func A() string {
if IsSomeUsecase(){
return "b"
}
return "a"
}
func TestA(t *testing.T) {
if IsSomeUsecace() {
assert.Equal(t, "b", A())
}
assert.Equal(t, "a", A())
}
この例ではそこまで問題になりませんが、ユースケースが増えて大量の条件分岐が発生すると、テストもより複雑化し、きちんとテストケースが網羅できているのか把握しにくくなると思います。また、機能ごとに関数が切られていないとテーブルドリブンテストが書きずらく、無理やり書いた挙句にとても読みづらいテストになってしまいます。
ですので、自分は以下のように多少冗長でもユースケースごとに関数を切った方がテストという観点でも良いかなと思います。
func A() string {
return "a"
}
func TestA(t *testing.T) {
assert.Equal(t, "a", A())
}
func B() string {
return "b"
}
func TestB(t *testing.T) {
assert.Equal(t, "b", B())
}
例がシンプルなので当たり前感がありますが、これが割と長い関数だとしたらシュッと条件分岐を後から加えがちなので、よく考えた方が良さそうですよね。
2. 外部APIへの通信を含む処理はモックを立てて、それを引数に渡す
外部APIへ通信しに行くクライアントとモック用のクライアントをInterfaceを利用して切り替えるというよくある手法です。
例えば、適当なAPI Clientを定義し、それを利用する関数を用意したとします。(内容は適当です)
type Client {
httpClient *http.Client
Config *Config
}
func NewClient(cfg config.Config) *Client {
return &Client{
httpClient: &http.Client{}
Config: cfg.SomeAPIConfig,
}
}
func (cli *Client) Call() string {
req, _ := http.NewRequest(cli.Config.Method, cli.Config.URL.String(), nil)
res, _ := cli.httpClient.Do(req)
return res.StatusCode
}
// 途中で外部APIにリクエストする処理
func SomeFunc(cli *Client) error {
resCode := cli.Call()
if resCode =! http.StatusOK{
return errors.New("error")
}
// ... 何らかの処理
return nil
}
このままSomeFunc()のテストを書いた場合、実際にAPIにリクエストをしてしまうので、以下のようにSomeFunc()が受け取る引数をinterfaceにして、テスト時にMockに差し替えれば良い感じにテストができます。
// Call() という振る舞いを提供するインターフェース
type APIClient interface {
Call() string
}
type MockClient {}
// 適当な値を返す
func (cli *MockClient) Call() string {
return http.StatusOK
}
// テスト時はcfg.Envが"test"となり、モックが返る
func NewClient(cfg config.Config) APIClient {
if cfg.Env == "test" {
return &MockClient{}
}
return &Client{
httpClient: &http.Client{}
Config: cfg.SomeAPIConfig,
}
}
// 引数をinterfaceにする
func SomeFunc(cli APIClient) error {
// ... 何らかの処理
}
テスト内容はこんな感じ
func TestSomeFunc(t *testing.T) {
// MockClient
client := NewClient(newTestConfig())
if err := SomeFunc(client); err != nil {
t.Fatal(err)
}
}
こんな感じでモックを使うと外部APIの影響を受けずにテストを書けるので、interfaceは使い勝手が良いなあという気持ちだった(実際にプロダクションコードでこうやってテストを書いてる)のですが、この記事を書くにあたり色々調べていたら、こんなものを発見しました。
https://github.com/golang/go/wiki/CodeReviewComments#interfaces
これを読む限り、どうやら実装側(実際にClientを定義/実装する側って意味かな)ではモックのためにinterfaceで公開するのではなく、実装をそのまま公開する方が良いとのこと。そして、利用側で適宜interfaceを定義して、テストしてくれ!って書いてあります。
上の自分が書いた例では、同じパッケージに全て詰め込んでいるので、実装側と利用側でパッケージを分けて、interfaceは利用側で定義してあげるようにした方が良さそうです。(この辺りまだ完全に腹落ちできてないところがあるので、なんかツッコミあればください)
3. カバレッジを見る
これはテストを書くという内容から少し離れてしまいますが、テストのカバレッジを可視化できるCodecovというサービスについて最後に少しだけ書きます。
カンムでは一部のバックエンドサービスで、Codecovを使ってカバレッジを計測してます。大きめの変更を加えた時に、カバレッジが落ちているかどうかをPRで指摘しやすいし、テストを書くモチベーションにも繋がるので個人的に良いなと思ってます。
CodecovはTokenを取得して、.circleci/config.ymlに簡単なスクリプトを書けば、CIからレポートをあげてくれて、それをGithubのPRのコメントから見れます。使い方は沢山記事が上がってたので、ここでは割愛します。
割と簡単に導入できて、テストがちゃんと書けているか確認できる良いツールなので是非使ってみてください。
最後に
Goは標準のtestingパッケージだけでテストを書けて良いですよね。(と言いながら、github.com/alecthomas/assertには大変お世話になっておりますが)
あまり共感されないかもしれないですが、フレームワークなどを使わずにゴリゴリGoでテストを書いて、それが通った時は何とも言えない気持ちさがあるので自分は好きです。
と余談はここまでにして、最後まで読んでいただきありがとうございます。
明日以降のアドベントカレンダーもぜひ見て、興味ある方は訪ねてみてくださいね。ではでは。
この記事が気に入ったらサポートをしてみませんか?