11.3 Goでどのようにテストを書くか

プログラムの開発においてテストはとても重要です。どのようにコードの質を保証するか、どのように各関数が実行できることを保証するか、また書いたコードの性能が良いことをどのように保証するかです。我々はユニットテストは主にプログラムの設計や実装のロジックエラーを発見することであると知っています。問題を早期に発見し、問題を特定し解決せしめ、性能をテストするにはプログラム設計上の問題のいくつかを発見することで、オンラインのプログラムがマルチプロセッシングしている状況でも安定を保てるようにします。この節ではこの一連の問題からGo言語でどのようにユニットテストと性能テストを実現するかご紹介します。

Go言語はあらかじめ用意されている軽量なテストフレームワークtestinggo testコマンドを使ってユニットテストと性能テストを実現します。testingフレームワークとその他の言語でのテストフレームワークはよく似ています。このフレームワークに基いて対応する関数に対してテストを書くことができます。またこのフレームワークに基づいて対応する耐久テストを書くこともできます。ではどのように書くのか一つ一つ見ていくことにしましょう。

どのようにテストを書くか

go testコマンドでは対応するディレクトリ下の全てのファイルを実行するしかできません。そのため、gotestというディレクトリを新規に作成することで、すべてのコードとテストコードをこのディレクトリの中に配置することにします。

次にこのディレクトリの下に2つのファイルを新規に作成します:gotest.goとgotest_test.go

  1. gotest.go:このファイルにはパッケージを一つ書きます。中身は除算を行う関数がひとつあります:

     package gotest
    
     import (
         "errors"
     )
    
     func Division(a, b float64) (float64, error) {
         if b == 0 {
             return 0, errors.New("除数は0以外でなければなりません")
         }
    
         return a / b, nil
     }
    
  2. gotest_test.go:これはユニットテストのファイルですが、以下の原則を覚えておいてください:

    • ファイル名は必ず_test.goが最後につくようにしてください。これによってgo testを実行した時に対応するコードが実行されるようになります。
    • testingというパッケージをimportする必要があります。
    • すべてのテスト関数名はTestから始まります。
    • テストはソースコードに書かれた順番に実行されます。
    • テスト関数TestXxx()のパラメータはtesting.Tです。この型を使ってエラーやテストの状態を記録することができます。
    • テストフォーマット: func TestXxx (t *testing.T)Xxxの部分は任意の英数字の組み合わせです。ただし頭文字は小文字[a-z]ではいけません、例えばTestintdivというのは間違った関数名です。
    • 関数ではtesting.TErrorErrorfFailNowFatalFatalIfメソッドをコールすることでテストがパスしないことを説明します。Logメソッドをコールすることでテストの情報を記録します。

      以下は我々のテストコードです:

      package gotest

      import (

        "testing"
      

      )

      func Test_Division_1(t *testing.T) {

        if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
            t.Error("除算関数のテストが通りません") // もし予定されたものでなければエラーを発生させます。
        } else {
            t.Log("はじめのテストがパスしました") //記録したい情報を記録します
        }
      

      }

      func Test_Division_2(t *testing.T) {

        t.Error("パスしません")
      

      }

      プロジェクトのディレクトリにおいてgo testを実行すると以下のような情報が表示されます:

      --- FAIL: Test_Division_2 (0.00 seconds)

        gotest_test.go:16: パスしません
      

      FAIL exit status 1 FAIL gotest 0.013s この結果が示すようにテストをパスしないのは、2つ目のテスト関数でテストが通らないコードt.Errorを書いていたからです。では1つ目の関数が実行した状況はどうでしょうか?デフォルトではgo testを実行するとテストがパスする情報は表示されません。go test -vというオプションを追加する必要があります。このようにすると以下の情報が表示されます:

      === RUN Test_Division_1 --- PASS: Test_Division_1 (0.00 seconds)

        gotest_test.go:11: 1つ目のテストがパス
      

      === RUN Test_Division_2 --- FAIL: Test_Division_2 (0.00 seconds)

        gotest_test.go:16: パスしません
      

      FAIL exit status 1 FAIL gotest 0.012s 上の出力はこのテストのプロセスを詳細に表示しています。テスト関数1Test_Division_1ではテストが通りました。しかし関数2Test_Division_2のテストは失敗しました。最後にテストが通らないという結論を得ました。以降ではテスト関数2を以下のようなコードに修正します:

      func Test_Division_2(t *testing.T) {

        if _, e := Division(6, 0); e == nil { //try a unit test on function
            t.Error("Division did not work as expected.") // 予期したものでなければエラーを発生
        } else {
            t.Log("one test passed.", e) //記録したい情報を記録
        }
      

      }
      その後go test -vを実行すると以下のような情報を表示してテストがパスします:

      === RUN Test_Division_1 --- PASS: Test_Division_1 (0.00 seconds)

        gotest_test.go:11: 1つ目のテストがパス
      

      === RUN Test_Division_2 --- PASS: Test_Division_2 (0.00 seconds)

        gotest_test.go:20: one test passed. 除数は0以外
      

      PASS ok gotest 0.013s

どのようにして耐久テストを書くか

耐久テストは関数(メソッド)の性能を測るために用いられます。ここでは再掲しませんが、ユニットテストを書くのと同じようなものです。ただし以下のいくつかに注意しなければなりません:

  • 耐久テストは以下のループの形式で行われなければなりません。この中でXXXは任意の英数字の組み合わせです。ただし、頭文字は小文字ではいけません。

      func BenchmarkXXX(b *testing.B) { ... }
    
  • go testはデフォルトで耐久テストの関数を実行しません。もし耐久テストを実行したい場合はオプション-test.benchを追加します。文法:-test.bench="test_name_regex"。例えばgo test -test.bench=".*"はすべての耐久テスト関数をテストすることを表します

  • 耐久テストではテストが正常に実行されるよう、ループの中においてtesting.B.Nを使用することを覚えておいてください
  • ファイル名はかならず_test.goで終わります

以下ではwebbench_test.goという名前の耐久テストファイルを作成します。コードは以下の通り:

package gotest

import (
    "testing"
)

func Benchmark_Division(b *testing.B) {
    for i := 0; i < b.N; i++ { //use b.N for looping 
        Division(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
    b.StopTimer() //调用该函数停止压力测试的时间计数

    //做一些初始化的工作,例如读取文件数据,数据库连接之类的,
    //这样这些时间不影响我们测试函数本身的性能

    b.StartTimer() //重新开始时间
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

go test -file webbench_test.go -test.bench=".*"というコマンドを実行すると、以下のような結果が現れます:

PASS
Benchmark_Division    500000000             7.76 ns/op
Benchmark_TimeConsumingFunction    500000000             7.80 ns/op
ok      gotest    9.364s    

上の結果は我々がどのようなTestXXXなユニットテスト関数も実行していないことを示しています。表示される結果は耐久テスト関数のみを実行しただけです。第一行にはBenchmark_Divisionが500000000回実行され示し、毎回の実行が平均で7.76ミリ秒であったことを示しています。第二行はBenchmark_TimeConsumingFunctinが500000000回実行され、毎回の平均実行時間が7.80ミリ秒であったことを示しています。最後の1行は全体の実行時間を示しています。

まとめ 

上のユニットテストと耐久テストの学習を通じて、testingパッケージが非常に軽量で、ユニットテストと耐久テストを書くのは非常に簡単であるとわかりました。ビルトインのgo testコマンドを組み合わせることで、非常に便利にテストを行うことができます。このように我々が毎回コードを修正し終わる度に、go testを実行するだけで簡単に回帰テストを行うことができます。

results matching ""

    No results matching ""