3104号室

本のレビューや音楽のことや日々思っていることを書くところ。つまり雑記。

Go言語で価値のあるUnitTestの書き方

f:id:masashi-yamada0110:20180921223019p:plain

目次

はじめに

僕はGo言語が好きで、よく使っています。

もちろん作ったものに対しては単体テストをきちんと書くし、そこそこ良いコードをかけている

…と思っていた頃が僕にもありました。

しかし、そんな幻想から一瞬で目が覚める出来事がありました。

それは・・・!

f:id:masashi-yamada0110:20180913205636p:plain

そう、泣く子も黙るTDDの第一人者@t_wadaさんです。

最近、幸運にもt-wadaさんの「テスト駆動開発」の講義を受ける機会がありました。

そのあと勉強のためにその講義の内容を自分なりに昇華し、Go言語でテストを書いてみたりしたのですが 思わず今まで書いたテストコードを火にくべて懺悔したくなるくらい、 「わかりやすく」かつ「保守しやすい」コードがGo言語でもかけたので紹介します。

テスト対象のプログラム

FizzBuzz問題を扱いテストを書いてみたいと思います。

FizzBuzz問題とは

FizzBuzz問題は以下のごく簡単な問題をさします。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

Fizz-Buzz問題とは - はてなキーワード

本体コード

FizzBuzzの数字から文字列に変換する部分を 構造体にするとこんな感じになりますね。

package fizzbuzz
import (
  "strconv"
)

type FizzBuzz struct {}

func (f *FizzBuzz)Convert(value int) string{
  if value % 3 == 0 && value % 5 == 0 {
    return "FizzBuzz"
  } 

  if value % 3 == 0 {
    return "Fizz"
  }

  if value % 5 == 0 {
    return "Buzz"
  }

  return strconv.Itoa(value)
}

このConvert関数に対してテストを書いていきます。

今までどんなテストを書いていたか

今まで書いていたテストコードは以下のようなものでした。

package fizzbuzz
import (
  "testing"
)

func TestFizzBuzz(t *testing.T){
  fizzbuzz := &FizzBuzz{}

  // 数字1を文字列1に変換する
  expect := "1"
  actual := fizzbuzz.Convert(1)
  if expect != actual {
    t.Errorf("expect: %v / actual: %v", expect, actual)
  }

  // 数字3を文字列Fizzに変換する
  expect = "Fizz"
  actual = fizzbuzz.Convert(3)
  if expect != actual {
    t.Errorf("expect %v / actual %v", expect, actual)
  }

  // 数字10を文字列Buzzに変換する
  expect = "Buzz"
  actual = fizzbuzz.Convert(10)
  if expect != actual {
    t.Errorf("expect %v / actual %v", expect, actual)
  }

  // 数字15を文字列FizzBuzzに変換する
  expect = "FizzBuzz"
  actual = fizzbuzz.Convert(15)
  if expect != actual {
    t.Errorf("expect %v / actual %v", expect, actual)
  }

}

「テストはベタに書いた方が良い」とか「テストは重複しても気にしなくても良い」だとかよくわからない思い込みがあってこんなコードをよくかいていました。

これを実行すると下記のような結果が得られます。

$ go test ./fizzbuzz -v
=== RUN   TestFizzBuzz
--- PASS: TestFizzBuzz (0.00s)
PASS
ok      _/Users/masashi/workspace/fizzbuzz/fizzbuzz 0.007s

失敗した場合はこんな感じに出力されます。

$ go test ./fizzbuzz -v
=== RUN   TestFizzBuzz
--- FAIL: TestFizzBuzz (0.00s)
    fizzbuzz_test.go:34: expect FizzBuzz / actual Fizz
FAIL
FAIL    _/Users/masashi/workspace/fizzbuzz/fizzbuzz 0.010s

今読み返すと、テスト結果から失敗した内容が読み取れず、「テストがちゃんと仕事してないなー。」と思うのですが、 つい最近までこれが普通だと思ってたんです。慣れって怖いものです。

取り入れた考え方

以下のような考え方を取り入れてテストを書き換えてみました。

テスト関数は機能ごとに分けて、わかりやすい関数名にする(できれば日本語で)

まず今まで僕が書いていたコードの一番悪い点は一つの関数で全てのテストを行ってしまっていることです。

これではコードを読む人は長い関数を全て目を通さなければテストが正しく動いているかわかりません。

しかもテストが失敗した時は行番号しか表示されないので、 いちいちソースコードを開いて失敗した原因を調べなければ失敗した昨日がわかりません。

なので関数は機能単位で分け、関数は"テスト対象の機能は何か”がすぐにわかる名前にします。

Go言語では関数名に日本語が使えるので特に制約がなければ日本語にしちゃいましょう。

例えば以下のような関数を用意します。

func Test_3の倍数はFizzと変換する(t *testing.T){
//ここにテストを書く
}

するとテストが失敗した場合は以下の出力になるので、どこで失敗したのかすぐにわかります。

--- FAIL: Test_3の倍数はFizzと変換する (0.00s)
    fizzbuzz_test.go:16: 失敗

テストを構造化する

テストの内容は構造化できることが多く、しかもその構造をテストコードに取り入れることで、 目的が明確なテストが書けるようになります。

例えばFizzBuzz構造体のConvert関数のテスト仕様は以下のツリー構造で表せます。

- 3の倍数はFizzと変換する 
  - 3をFizzに変換する 
  - 6をFizzに変換する 
- 5の倍数はBuzzと変換する
  - 5をBuzzに変換する
  - 10をBuzzに変換する
- 3と5両方の倍数はFizzBuzzと変換する
  - 15をFizzBuzzに変換する
  - 30をFizzBuzzに変換する
- そのほかの数字は文字列に変換する
  - 1を"1"に変換する
  - 2を"2"に変換する

この通りにテストコードが構造化されていると、仕様と対応して見やすいものになるはずです。

これにはGo言語のtestingパッケージのRun()関数を使いましょう。

https://golang.org/pkg/testing/#T.Run

func Test_3の倍数はFizzと変換する(t *testing.T){
    t.Run("3をFizzに変換する", func(t *testing.T){
      expect := "Fizz"
      actual := fizzbuzz.Convert(3)
      if expect != actual {
        t.Errorf("expect %v / actual %v", expect, actual)
      }
    })
}

上記のようにコードを記述すると、出力は以下のようになります。

$ go test ./fizzbuzz -v
=== RUN   Test_3の倍数はFizzと変換する
=== RUN   Test_3の倍数はFizzと変換する/3をFizzに変換する
--- PASS: Test_3の倍数はFizzと変換する (0.00s)
    --- PASS: Test_3の倍数はFizzと変換する/3をFizzに変換する (0.00s)

これで出力結果が仕様に対応し、より意味を汲み取りやすいものとなりました。

テストコードの重複をなくす

テストコードに重複があると、コードが長くなりコードを読む人に負担をかけます。

テストコードも本体コードと同様に重複をなくすようにします。

このときに使えるのがGo言語で推奨されているTable Driven Testです。

本家Wikiは以下にTeble Driven Testについて書かれています。 github.com

日本語だと以下がわかりやすいと思います。 qiita.com

Table Driven テストを用いると コードの重複を減らし、スマートにテストを書くことができますので是非マスターしましょう。

Go言語でテストを書く時は必須のスキルです。

書き換えたテストコードと実行結果

最終的なコードはこのようになります。

package fizzbuzz

import (
    "testing"
)

var fizzbuzz = &FizzBuzz{}

func Compare(t *testing.T, expect, actual interface{}) {
    if expect != actual {
        t.Errorf("値が異なります。\n Expect:%s \n Actual:%s", expect, actual)
    }
}

func Test_3の倍数はFizzと変換する(t *testing.T) {
    testCases := []struct {
        name   string
        input  int
        expect string
    }{
        {"3をFizzに変換する", 3, "Fizz"},
        {"6をFizzに変換する", 6, "Fizz"},
    }
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            Compare(t, tc.expect, fizzbuzz.Convert(tc.input))
        })
    }
}

func Test_5の倍数はBuzzと変換する(t *testing.T) {
    testCases := []struct {
        name   string
        input  int
        expect string
    }{
        {"5をBuzzに変換する", 5, "Buzz"},
        {"10をBuzzに変換する", 10, "Buzz"},
    }
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            Compare(t, tc.expect, fizzbuzz.Convert(tc.input))
        })
    }
}

func Test_3と5両方の倍数はFizzBuzzと変換する(t *testing.T) {
    testCases := []struct {
        name   string
        input  int
        expect string
    }{
        {"15をFizzBuzzに変換する", 15, "FizzBuzz"},
        {"30をFizzBuzzに変換する", 30, "FizzBuzz"},
    }
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            Compare(t, tc.expect, fizzbuzz.Convert(tc.input))
        })
    }
}

func Test_そのほかの数字は文字列に変換する(t *testing.T) {
    testCases := []struct {
        name   string
        input  int
        expect string
    }{
        {`1を"1"に変換する`, 1, "1"},
        {`2を"2"に変換する`, 2, "2"},
    }
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            Compare(t, tc.expect, fizzbuzz.Convert(tc.input))
        })
    }
}

実行すると以下のような実行結果が得られます。

$ go test ./fizzbuzz -v
=== RUN   Test_3の倍数はFizzと変換する
=== RUN   Test_3の倍数はFizzと変換する/3をFizzに変換する
=== RUN   Test_3の倍数はFizzと変換する/6をFizzに変換する
--- PASS: Test_3の倍数はFizzと変換する (0.00s)
    --- PASS: Test_3の倍数はFizzと変換する/3をFizzに変換する (0.00s)
    --- PASS: Test_3の倍数はFizzと変換する/6をFizzに変換する (0.00s)
=== RUN   Test_5の倍数はBuzzと変換する
=== RUN   Test_5の倍数はBuzzと変換する/5をBuzzに変換する
=== RUN   Test_5の倍数はBuzzと変換する/10をBuzzに変換する
--- PASS: Test_5の倍数はBuzzと変換する (0.00s)
    --- PASS: Test_5の倍数はBuzzと変換する/5をBuzzに変換する (0.00s)
    --- PASS: Test_5の倍数はBuzzと変換する/10をBuzzに変換する (0.00s)
=== RUN   Test_3と5両方の倍数はFizzBuzzと変換する
=== RUN   Test_3と5両方の倍数はFizzBuzzと変換する/15をFizzBuzzに変換する
=== RUN   Test_3と5両方の倍数はFizzBuzzと変換する/10をFizzBuzzに変換する
--- PASS: Test_3と5両方の倍数はFizzBuzzと変換する (0.00s)
    --- PASS: Test_3と5両方の倍数はFizzBuzzと変換する/15をFizzBuzzに変換する (0.00s)
    --- PASS: Test_3と5両方の倍数はFizzBuzzと変換する/10をFizzBuzzに変換する (0.00s)
=== RUN   Test_そのほかの数字は文字列に変換する
=== RUN   Test_そのほかの数字は文字列に変換する/1を"1"に変換する
=== RUN   Test_そのほかの数字は文字列に変換する/2を"2"に変換する
--- PASS: Test_そのほかの数字は文字列に変換する (0.00s)
    --- PASS: Test_そのほかの数字は文字列に変換する/1を"1"に変換する (0.00s)
    --- PASS: Test_そのほかの数字は文字列に変換する/2を"2"に変換する (0.00s)
PASS
ok      _/Users/masashi/workspace/fizzbuzz/fizzbuzz 0.008s

テストが出力する結果から仕様がきちんと読み取れます。

もしテストが失敗した場合、その箇所は以下のように表示されます。

--- FAIL: Test_3と5両方の倍数はFizzBuzzと変換する (0.00s)
    --- FAIL: Test_3と5両方の倍数はFizzBuzzと変換する/15をFizzBuzzに変換する (0.00s)
        fizzbuzz_test.go:11: 値が異なります。
             Expect:FizzBuzz 
             Actual:Fizz

どこで失敗したのか一目瞭然ですね。

このようにテスト関数をわかりやすい名前(可能であれば日本語)でテストを構造化することで、テストが仕様を明確に示すようになり、テストが失敗した時もすぐに発生箇所を見つけられるようになります。

これはチームで開発する場合や長く保守するソフトウェアを開発するときに効果を発揮すること間違いなしです。

是非試してみてください。

テスト駆動開発

テスト駆動開発