少なくともpublicなメソッドや関数のテストケースがないと、ロクにリファクタもできず、機能追加ではデグレが生まれ、エンジニアのモチベーションが下がり続けるというのは世界の理(ことわり)。
まあでもレガシーシステムが横行する世知辛い現実業務では【テストコードが無い】状態なんて往々にしてあるわけで…
- 「動いたものが仕様!な世紀末」状態
- 「短納期で要件がコロコロ変わりテストケース書く前にリリースになっちゃったよ!」状態
- 「結合試験はやったし余裕があるときにテストコード書こう~そして余裕はこない~」状態
- 「カバレッジに拘った結果膨大になりすぎたテストケースの保守工数が逆にコストになりすぎて放棄」状態
- 「誰も触りたくない改修したら負けなレガシーシステム」状態
理由は様々。
それでも踏ん張ってテストコードは書かないと、苦しむのは未来の自分達。
なんだか泣けてきた…。
本題からズレましたが、Goも触り始めて2週間が経過し ちょうどいいロジックもできたので今後はテストケースを書いていこうと思います。
##今回やりたいこと
- goのテストコードを書いてみる
-
go test
でテストの実行をしてみる - 未カバレッジの行を特定し、カバレッジ率を100%にしてみる
##前提条件
- 過去の記事「WindowsのEclipseでGo+AppEngineの開発環境を構築」で構築した環境を利用しています。
- 前回作った「Go言語でn元連立1次方程式の解と逆行列を求めてみる(掃き出し法編)」がテスト対象です。
##コーディング開始
fox/matrix/matrix_text.go
matrix.go
のテストケース。
本来は引数のindexがlengthを超えているパターン等も用意すべきですが、そもそも実装側でケアできていないので正常系のみのケースです。
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差を許容するニアリーイコールでの比較にしました。
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だとかなり見やすいです)
デフォルトブラウザで以下のように表示されます。
この赤い行がテスト未通過のコードです。
調べてみたら、実装時にリファクタした結果「絶対にif文が成立しない不要コード」となっていたため、削除しました。
やっぱ自動テストは最高ですね!
このコードは色々機能追加予定なので安心してリファクタが行えます。
そして黄金郷(エル・ドラド)へ
上記の不要コードを削除し、テストコードを再実行。
ok fox/matrix 0.158s coverage: 100.0% of statements
きたーカバレッジ100%!!
まあ100%なんて夢を見れるのはこの程度の小さなプログラムだけですね。
Javaとかだと例えプログラムが小さくてもcatch内のException等で諦めるしかない時もあるし。
感想
とりあえず、Goでテストコードを書いてみた感想ですが…何も便利なものがないですね笑
まあそもそも言語自体がシンプルなので高機能なテストツール自体不要なのかもしれません。
これから先使い続けると色々見えててきそう、かな。
~ではまた次回~