初めに
単体テストを行なっている途中、Go言語のスライスの初期化について注意しなければいけないなと思ったので記事にしました
結論から申し上げますとGo言語のスライスはmake()
で初期化をすることでパニックを未然に防ぐことができるという内容になっています
slice := make([]*int64, 0)
スライスの初期化が間違っている改善前のファイル
outputのinterfaceが定義されたファイル
package output
import "github.com/pkg/domain/model"
type FailedRow struct {
ID string `json:"id"`
Cause string `json:"cause"`
}
type TaskboardUploadPresenter interface {
Output(failedRows []*model.FailedRow) ([]*FailedRow, error)
Error(err error) ([]*FailedRow, error)
}
Outputメソッドの実装ファイル
func (i exampleStruct) Output(failedRows []*model.FailedRow) ([]*output.FailedRow, error) {
var failedRowsOutput []*output.FailedRow
for _, failedRow := range failedRows {
failedRowsOutput = append(failedRowsOutput, &output.FailedRow{
ReportNo: failedRow.ID,
Cause: failedRow.Cause,
})
}
return failedRowsOutput, nil
}
exampleStruct.Outputメソッドの単体テスト
func TestExampleStructOutputEmpty(t *testing.T) {
exampleStructPresenter := presenter.NewExampleStructPresenter()
failedRows := []*model.FailedRow{}
expectedOutput := []*output.FailedRow{}
actual, err := exampleStructPresenter.Output(failedRows)
assert.NoError(t, err)
assert.Equal(t, expectedOutput, actual)
}
改善前のファイルの単体テストの結果
Error Trace: ~/pkg/interfaces/presenter/example_struct_test.go:40
Error: Not equal:
expected: []*output.FailedRow{}
actual : []*output.FailedRow(nil)
Diff:
--- Expected
+++ Actual
@@ -1,3 +1,2 @@
-([]*output.FailedRow) {
-}
+([]*output.FailedRow) <nil>
Test: TestexampleStructOutputEmpty
--- FAIL: TestexampleStructOutputEmpty (0.00s)
Expected :[]*output.FailedRow{}
Actual :[]*output.FailedRow(nil)
この2行のコードは、異なるタイプの空のスライスを表しています:
expectedOutput := []*output.FailedRow{}
- 要素がゼロの空のスライスを作成します。これは有効なスライスであり、追加したり反復したりすることができます。
- スライスはGo言語における動的配列の一種です。スライスは固定長の配列とは異なり、要素の追加や削除が容易に行えます。下記の記述はスライスの基本的な特徴です。
スライスの特徴- 動的サイズ: スライスは必要に応じてサイズを変更できます。
- 参照型: スライスは元の配列を参照するため、スライスを変更すると元の配列にも影響します。
- 内部構造: スライスは3つの要素から構成されます。
- ポインタ: 元の配列のデータを指すポインタ
- 長さ: スライスの要素数
- 容量: スライスが参照する配列の容量
expectedOutput := []*output.FailedRow(nil)
- これはnilスライスとなります。これは空のスライスとは異なりますが、ほとんどの場合同じように動作します。
- しかしこれはnilであり要素がゼロの実際のスライスではありません。
- 空のスライス([]*output.FailedRow{})を使用する方がわかりやすいです。
- 下記のコードで、nilSliceはnilであり、emptySliceは空のスライスであることを確認できます。どちらも長さと容量は0です。nilスライスを使うことのデメリットとしては、誤って操作しようとするとパニックが発生する可能性があります。空のスライスを使うことで、このリスクを回避できます。
package main
import "fmt"
func main() {
var nilSlice []int
emptySlice := []int{}
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
fmt.Println(cap(nilSlice)) // 0
fmt.Println(cap(emptySlice)) // 0
}
つまり単体テストはこのように書けばテストが通ることになりますが、パニックが発生する原因を残しておくわけにはいきません。
func TestExampleStructOutputEmpty(t *testing.T) {
exampleStructPresenter := presenter.NewExampleStructPresenter()
failedRows := []*model.FailedRow{}
expectedOutput := []*output.FailedRow(nil)
actual, err := exampleStructPresenter.Output(failedRows)
assert.NoError(t, err)
assert.Equal(t, expectedOutput, actual)
}
改善後の実装ファイル
スライスの初期化にmake()
を使うよう修正しました。
func (i exampleStruct) Output(failedRows []*model.FailedRow) ([]*output.FailedRow, error) {
failedRowsOutput := make([]*output.FailedRow, 0, len(failedRows))
for _, failedRow := range failedRows {
failedRowsOutput = append(failedRowsOutput, &output.FailedRow{
ReportNo: failedRow.ID,
Cause: failedRow.Cause,
})
}
return failedRowsOutput, nil
}
テストも無事成功しました。
まとめ
単体テストをしたことによりパニックの発生を未然に防ぐことができて良かったです
スライスのポインタ、長さ、容量についてより深く学ぶきっかけにもなったので機会に恵まれたなと思います。