LoginSignup
11
8

More than 5 years have passed since last update.

Goのテスト作成とカバレッジ率&カバレッジ行表示をしてみる

Last updated at Posted at 2016-11-13

少なくともpublicなメソッドや関数のテストケースがないと、ロクにリファクタもできず、機能追加ではデグレが生まれ、エンジニアのモチベーションが下がり続けるというのは世界の理(ことわり)。

まあでもレガシーシステムが横行する世知辛い現実業務では【テストコードが無い】状態なんて往々にしてあるわけで…

  • 「動いたものが仕様!な世紀末」状態
  • 「短納期で要件がコロコロ変わりテストケース書く前にリリースになっちゃったよ!」状態
  • 「結合試験はやったし余裕があるときにテストコード書こう~そして余裕はこない~」状態
  • 「カバレッジに拘った結果膨大になりすぎたテストケースの保守工数が逆にコストになりすぎて放棄」状態
  • 「誰も触りたくない改修したら負けなレガシーシステム」状態

理由は様々。
それでも踏ん張ってテストコードは書かないと、苦しむのは未来の自分達。
なんだか泣けてきた…。

本題からズレましたが、Goも触り始めて2週間が経過し ちょうどいいロジックもできたので今後はテストケースを書いていこうと思います。

今回やりたいこと

  1. goのテストコードを書いてみる
  2. go testでテストの実行をしてみる
  3. 未カバレッジの行を特定し、カバレッジ率を100%にしてみる

前提条件

パッケージ構成は以下。
ecliseの設定_07.PNG

コーディング開始

fox/matrix/matrix_text.go

matrix.goのテストケース。
本来は引数のindexがlengthを超えているパターン等も用意すべきですが、そもそも実装側でケアできていないので正常系のみのケースです。

fox/matrix/matrix_text.go
package matrix

import "testing"

func TestMultiplication(t *testing.T) {
    testCases := []struct {
        left   [][]float64
        right  [][]float64
        result [][]float64
    }{
        {
            [][]float64{{1, 5, 9}},
            [][]float64{{5, 2}, {3, 1}, {4, -1}},
            [][]float64{{56, -2}},
        },
        {
            [][]float64{{1, 5, 9}},
            [][]float64{{8, 4, 2}, {1, 3, -6}, {-7, 0, 5}},
            [][]float64{{-50, 19, 17}},
        },
        {
            [][]float64{{5}, {2}},
            [][]float64{{1, 5, 9}},
            [][]float64{{5, 25, 45}, {2, 10, 18}},
        },
    }

    for _, tc := range testCases {
        result := Multiplication(tc.left, tc.right)
        if !equals(result, tc.result) {
            t.Errorf("not equals. %#v %#v", result, tc.result)
        }
    }
}

func TestDeepCopy(t *testing.T) {
    testCases := []struct {
        matrix [][]float64
    }{
        { //1行しかないパターン
            [][]float64{{1, 5, 9}},
        },
        { //3×3の行列
            [][]float64{{8, 4, 2}, {1, 3, -6}, {-7, 0, 5}},
        },
    }

    for _, tc := range testCases {
        result := DeepCopy(tc.matrix)
        if !equals(result, tc.matrix) {
            t.Errorf("not equals. %#v %#v", result, tc.matrix)
        }
    }
}

func TestCreateRamdomMatrix(t *testing.T) {
    num := []int{5, 5}
    matrix := CreateRamdomMatrix(num)

    valid := bool(true)
    if len(matrix) != num[0] {
        valid = false
    }
    for i := range matrix {
        if len(matrix[i]) != num[1] {
            valid = false
        }
    }
    if !valid {
        t.Errorf("invalid.")
    }
}

func TestCreateUnitMatrix(t *testing.T) {
    num := 5
    matrix := CreateUnitMatrix(num)

    valid := bool(true)
    for i := range matrix {
        for n := range matrix[i] {
            if i == n && matrix[i][n] != 1 {
                valid = false
            } else if i != n && matrix[i][n] != 0 {
                valid = false
            }
        }
    }
    if !valid {
        t.Errorf("invalid.")
    }
}

func TestReplaceMatrixRow(t *testing.T) {
    testCases := []struct {
        matrix [][]float64
        index1 int
        index2 int
        result [][]float64
    }{
        { //入れ替わらないパターン
            [][]float64{{5, 2}, {3, 1}, {4, -1}},
            1,
            1,
            [][]float64{{5, 2}, {3, 1}, {4, -1}},
        },
        { //入れ替わるパターン
            [][]float64{{5, 2}, {3, 1}, {4, -1}},
            0,
            2,
            [][]float64{{4, -1}, {3, 1}, {5, 2}},
        },
    }

    for _, tc := range testCases {
        ReplaceMatrixRow(tc.matrix, tc.index1, tc.index2)
        if !equals(tc.matrix, tc.result) {
            t.Errorf("not equals. %#v %#v", tc.matrix, tc.result)
        }
    }
}

func equals(matrix1 [][]float64, matrix2 [][]float64) bool {
    if len(matrix1) != len(matrix2) {
        return false
    }
    for i := range matrix1 {
        if len(matrix1[i]) != len(matrix2[i]) {
            return false
        }
        for n := range matrix1[i] {
            if matrix1[i][n] != matrix2[i][n] {
                return false
            }
        }
    }
    return true
}

fox/matrix/gaussian_test.go

gaussian_test.goのテストケース。
こちらも「カバレッジを100%にする」ことを目的とした正常系のみのテストケースになります。
※本来は正則でない行列のパターンとかも追加すべき
また、どうしても丸め誤差が生じてしまうため、完全一致での比較ではなく、0.00000001差を許容するニアリーイコールでの比較にしました。

fox/matrix/gaussian_test.go
package matrix

import (
    "math"
    "testing"
)

func TestSolve(t *testing.T) {

    testCases := []struct {
        left    [][]float64
        right   [][]float64
        result  [][]float64
        inverse [][]float64
    }{
        {
            [][]float64{{3, 2}, {7, 5}},
            [][]float64{{1}, {-1}},
            [][]float64{{7}, {-10}},
            [][]float64{{5, -2}, {-7, 3}},
        },
        {
            [][]float64{{1, 2, 0}, {3, -1, 1}, {2, 1, 1}},
            [][]float64{{3}, {0}, {1}},
            [][]float64{{1}, {1}, {-2}},
            [][]float64{{0.5, 0.5, -0.5}, {0.25, -0.25, 0.25}, {-1.25, -0.75, 1.75}},
        },
        {
            [][]float64{{2, 1, -1}, {1, -1, 2}, {-2, 3, 1}},
            [][]float64{{1}, {5}, {7}},
            [][]float64{{1}, {2}, {3}},
            [][]float64{{0.35, 0.2, -0.05}, {0.25, 0, 0.25}, {-0.05, 0.4, 0.15}},
        },
    }
    for _, tc := range testCases {
        ge := NewGaussJordanElimination(tc.left, tc.right)
        if !nearlyEquals(ge.Solve(), tc.result) {
            t.Errorf("not equals. %#v %#v", ge.RfsMatrix, tc.result)
        }
        if !nearlyEquals(ge.InverseMatrix, tc.inverse) {
            t.Errorf("not equals. %#v %#v", ge.InverseMatrix, tc.inverse)
        }
    }
}

func nearlyEquals(matrix1 [][]float64, matrix2 [][]float64) bool {
    if len(matrix1) != len(matrix2) {
        return false
    }
    for i := range matrix1 {
        if len(matrix1[i]) != len(matrix2[i]) {
            return false
        }
        for n := range matrix1[i] {

            if matrix1[i][n] != matrix2[i][n] && math.Abs(matrix1[i][n]-matrix2[i][n]) > 0.00000001 {
                return false
            }
        }
    }
    return true
}

テストの実行

いきなりハマりました。Eclipseでのテスト実行がうまくいかず、今回は断念。
あきらめてコンソールでの実行にしました。
やっぱJava以外ではEclipseクソだわ

fox/matrixパッケージを対象にテストの実行を行ってみます。
コマンドプロンプトから実行する場合、eclipseの該当プロジェクトをGOPATHに追加する必要があるので気を付けてください。
setで追加しているためプロンプトを落とせばGOPATHは元に戻ります。

※下記{eclipse_project} は筆者の場合はC:\works\eclipse\workspace\matrix

コマンドプロンプトで実行
set GOPATH=%GOPATH%;"{eclipse_project}"
cd "{eclipse_project}"\src
go test -coverprofile=cover.out  fox\matrix

上記を実行すると以下のように表示されます。

ok      fox/matrix      0.099s  coverage: 98.5% of statements

カバレッジ率98.5%。どうやら通過していない行が存在するようです。
カバレッジツールを利用して未通過行を特定してみます。

カバレッジツールの利用

goのライブラリとしてtoolが用意されていたためそちらを利用しました。
go testを実行する際に、-coverprofile=cover.outというオプションを付けていたのがミソです。
実行した際のカレントディレクトリにcover.outというファイルができています。
このファイルをツールで読み込みます。

コマンドプロンプトから実行
go tool cover -html=cover.out

折角なのでHTMLで表示。(HTMLだとかなり見やすいです)
デフォルトブラウザで以下のように表示されます。

test_01.PNG

この赤い行がテスト未通過のコードです。
調べてみたら、実装時にリファクタした結果「絶対にif文が成立しない不要コード」となっていたため、削除しました。

やっぱ自動テストは最高ですね!
このコードは色々機能追加予定なので安心してリファクタが行えます。

そして黄金郷(エル・ドラド)へ

上記の不要コードを削除し、テストコードを再実行。

ok      fox/matrix      0.158s  coverage: 100.0% of statements

きたーカバレッジ100%!!

まあ100%なんて夢を見れるのはこの程度の小さなプログラムだけですね。
Javaとかだと例えプログラムが小さくてもcatch内のException等で諦めるしかない時もあるし。

感想

とりあえず、Goでテストコードを書いてみた感想ですが…何も便利なものがないですね笑
まあそもそも言語自体がシンプルなので高機能なテストツール自体不要なのかもしれません。
これから先使い続けると色々見えててきそう、かな。

~ではまた次回~

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8