0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

learn-go-with-tests 整数〜配列とスライス

Last updated at Posted at 2024-06-26

整数

integersというディレクトリを作成し、その中にadder_test.goを作成します。
TDDなのでまずはテストから書いていきます。

adder_test.go
package integers

import "testing"

func TestAdder(t *testing.T) {
	sum := Add(2, 2)
	want := 4

	if sum != want {
		t.Errorf("want '%d' but got '%d'", want, sum)
	}
}

これで実行すると、関数を作成していないので当然ながら失敗します。

./adder_test.go:6:9: undefined: Add

これだと、そもそもテストを実行できないので、テストを実行するための最小限のコードを記述し、あえて失敗したテスト出力を確認します。

adder.goをテストファイルと同じ階層に作成し、関数を作成します。

adder.go
package integers

func Add(x, y int) int{
	return 0
}

テストを実行すると、4が返却されることを期待しているので、当然こちらも失敗します。

adder_test.go:10: want '4' but got '0'

以下のように直すとテストがパスします。

adder.go
package integers

func Add(x, y int) int {
	return x + y
}

反復、繰り返し

Goで繰り返し作業を行うには、 forが必要です。 Goには while、do、 untilキーワードはなく、forのみ使用できます。

forの説明はこちらで書いているので参考にしてください。

こちらもまずはテストから書いていきます。当然パスしません。

repeat_test.go
package iteration

import "testing"

func TestRepeat(t *testing.T){
	repeated := Repeat("a")
	want := "aaaaa"

	if repeated != want {
		t.Errorf("expected %q but got %q", want, repeated)
	}
}

次に最低限の関数を作成します。
repeat_test.go:10: expected "aaaaa" but got ""で、パスしません。

repeat.go
package iteration

func Repeat(character string) string {
	return ""
}

次にテストをパスするよう修正します。
これでテストはパスします。

repeat.go
package iteration

func Repeat(character string) string {
	var repeated string
	for i := 0; i < 5; i++{
		repeated += character
	}
	return repeated
}

さらに、リファクタリングします。大したリファクタリングではありませんが、リピート回数を定数にします。

repeat.go
package iteration
const repeatCount = 5

func Repeat(character string) string {
	var repeated string
	for i := 0; i < repeatCount; i++{
		repeated += character
	}
	return repeated
}

ベンチマーク

ベンチマークテストは、特定の関数やコードのパフォーマンスを測定するために使用します。Go言語では、testingパッケージを使ってベンチマークを記述します。

ベンチマーク関数は、テスト関数と似たような形式で記述します。
関数名はBenchmarkで始め、*testing.B を受け取ります。

func BenchmarkRepeat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Repeat("a")
    }
}

*testing.B 型の引数 b は、ベンチマークの実行を管理するためのオブジェクトです。
b.Nは、ベンチマークが実行される回数を示します。これは、Goのベンチマークフレームワークが自動的に設定します。
ベンチマーク関数内でb.N回ループを実行し、その実行時間を計測します。

ベンチマークテストを実行するには、以下のコマンドを使います:

go test -bench=.

ベンチマークを実行すると、関数が b.N 回実行され、その合計時間を b.N で割って1回の実行にかかる平均時間を計測します。

下記の例では、Repeat 関数が 13503596 回実行され、1回あたりの平均実行時間が 86.93 ns となっています。

go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/username/my-project/Iteration
cpu: VirtualApple @ 2.50GHz
BenchmarkRepeat-8       13503596                86.93 ns/op
PASS
ok  

ベンチマークを使うことで、コードのボトルネックを見つけたり、最適化の効果を確認したりすることができます。

  • パフォーマンスの確認: コードの最適化前後のパフォーマンスを比較できます。
  • スケーラビリティ: 特定の操作が多く実行される環境で、どの程度の負荷がかかるかを理解できます。

配列とスライス

配列を使うと、同じ型の複数の要素を特定の順番で変数に格納することができます。

配列を持っていると、それらの要素を繰り返し処理しなければならないことがよくあります。

そこで、新しく知ったforの知識を使ってSum関数を作ってみましょう。
Sumは数値の配列を受け取り、その合計を返します。

まずはテストから作成します。
./sum_test.go:8:9: undefined: Sumとなり、テストは当然パスしません。

sum_test.go
package array

import "testing"

func TestSum(t *testing.T){
	numbers := [5]int{1,2,3,4,5}

	got := Sum(numbers)
	want := 15

	if got != want {
		t.Errorf("got %d want %d given, %v", got, want, numbers)
	}
}

次にテストを実行するための最低限の関数を用意します。

sum.go
package array

func Sum(numbers [5]int) int {
 return 0
}

これでテストを実行すると以下の通り失敗します。

sum_test.go:12: got 0 want 15 given, [1 2 3 4 5]

次にfor rangeを使用してテストをパスするコードに書き換えます。

package array

func Sum(numbers [5]int) int {
	sum := 0
	for _, v := range numbers{
		sum += v
	}
 return sum
}

これで無事テストがパスします。

引用
配列の興味深い特性として、サイズが型でエンコードされていることが挙げられます。 もし [5]int を期待する関数に [4]int を渡そうとしてもコンパイルできません。 これらは異なる型なので、int を求める関数に string を渡そうとするのと同じです。

配列の長さが固定されているのは非常に面倒だと思うかもしれませんし、ほとんどの場合、配列を使うことはないでしょう。

次に、配列のサイズを決めずに、スライスを使用して任意のサイズを持てるようにしましょう。

そのために、まずはテストを修正します。

package array

import (
	"testing"
)

func TestSum(t *testing.T){

	t.Run("collection of 5 numbers", func(t *testing.T) {
		numbers := []int{1,2,3,4,5}
	
		got := Sum(numbers)
		want := 15
	
		if got != want {
			t.Errorf("got %d want %d given, %v", got, want, numbers)
		}
	})

	t.Run("collection of any size of numbers", func(t *testing.T) {
		numbers := []int{1, 2, 3}

		got := Sum(numbers)
		want := 6

		if got != want {
			t.Errorf("got %d want %d given, %v", got, want, numbers)
		}
	})
}

この時点ではテストは失敗します。なぜなら関数の引数が配列だからです。
したがって、このようにfunc Sum(numbers []int)と引数をスライスに変更します。

これでテストはパスします。

次に、関数とテストをリファクタリングして、様々なスライスの数を受け取り、渡された各スライスの SumAll を含む新しいスライスを返します。

例えば、以下のようになります。

SumAll([]int{1,2}, []int{0,9}) は []int{3, 9}を返します。
もしくは
SumAll([]int{1,1,1}) は []int{3}を返します。

まずはテストから書きます。まだ関数を定義していないため失敗します。

sum_test.go
func TestSumAll(t *testing.T){

	got := SumAll([]int{1, 2}, []int{0, 9})
	want := []int{3, 9}

	if got != want{
		t.Errorf("got %v want %v", got, want)
	}
}

最低限の関数を用意します。

sum.go
func SumAll(numbersToSum ...[]int) (sums []int){
	return
}

実行すると何やらスライスはnilとしか比較できないというエラーで失敗します。

./sum_test.go:26:5: invalid operation: got != want (slice can only be compared to nil)

これはGoでは、スライスで等号演算子を使うことはできないことが原因です。

各スライスをループして比較することもできますが、より簡単なのはreflect.DeepEqualを使うことです。ただし、型安全ではないのでその点は注意が必要です。

reflectを使うために、インポートします。そして、以下のように書きます。

import "reflect"

if !reflect.DeepEqual(got, want) {
  t.Errorf("got %v want %v", got, want)
}

するとテスト実行に成功しsum_test.go:28: got [] want [3 9]と失敗します。

では、このテストをパスするために関数を修正します。
必要なのは可変長引数を繰り返し処理して、前に使ったSum関数を使って合計を計算し、それを返すスライスに追加することです。

以下の通り、引数でスライスを受け取り、それをループで回してSum関数で合計した結果を新たなスライスにappendで追加します。

これでテストがパスします。

sum.go
func SumAll(numbersToSum ...[]int) []int {
	var sums []int
	for _, numbers := range numbersToSum {
		sum := Sum(numbers)
		sums = append(sums, sum) 
	}
	return sums
}

appendの使い方などは以下を参考ください。

次の要件は SumAll を SumAllTails に変更することです。コレクションの末尾とは、最初のものを除いたすべてのアイテムのことです。

まずはテストを書き、失敗します。

sum_test.go
func TestSumAllTails(t *testing.T){
	got := SumAllTails([]int{1, 2, 3}, []int{0, 5, 9})
	want := []int{5, 14}

	if !reflect.DeepEqual(got, want) {
		t.Errorf("got %v want %v", got, want)
	}
}

次に関数を書きます。これでテストが通ります。

func SumAllTails(numbers ...[]int ) []int {
	var result [] int
	for _, v := range numbers{
		result = append(result, Sum(v[1:]))
	}
	return result
}

しかし、空のスライスを渡した場合はどうなるでしょうか。テストを追加、修正します。

func TestSumAllTails(t *testing.T){
	t.Run("make the sums of some slices", func (t *testing.T)  {
		got := SumAllTails([]int{1, 2, 3}, []int{0, 5, 9})
		want := []int{5, 14}
	
		if !reflect.DeepEqual(got, want) {
			t.Errorf("got %v want %v", got, want)
		}
	})

	t.Run("safely sum empty slices", func(t *testing.T) {
		got := SumAllTails([]int{}, []int{3, 4, 5})
		want := []int{0, 9}

		if !reflect.DeepEqual(got, want) {
				t.Errorf("got %v want %v", got, want)
		}
	})
}

すると、パニックを起こし失敗します。スライスには要素がないため、この参照は範囲外となり、パニックが発生します。

panic: runtime error: slice bounds out of range [1:0] 

したがって、スライスが空の場合は0と表示するように関数を修正します。
以下はスライスのlenが0の場合は0をスライスへ追加するようにしています。
これでテストがパスします。

func SumAllTails(numbers ...[]int ) []int {
	var result [] int
	for _, v := range numbers{
		if len(v) == 0 {
			result = append(result, 0)
		} else {
		result = append(result, Sum(v[1:]))
		}
	}
	return result
}

最後にテストコードも重複箇所があるのでリファクタリングします。
以下のようにエラー表示を関数に切り分け、該当箇所を変更します。

func checkSums(t *testing.T, got, want []int) {
	t.Helper()
	if !reflect.DeepEqual(got, want) {
		t.Errorf("got %v want %v", got, want)
	}
}
.
.
.
func TestSumAll(t *testing.T){
.
.
.
	checkSums(t, got, want)
}

テストカバー率

Goの組み込みテストツールキットには、 カバレッジツールがあり、カバーしていないコードの領域を特定するのに役立ちます。

go test -cover
%go test -cover  
PASS
coverage: 100.0% of statements

メモ

Errorfなどで%でフォーマットを指定するものは様々あるので、ここでメモ書き。

指定子 説明
%v 値のデフォルトフォーマット。構造体ではフィールド名も表示される
%+v 構造体の場合、フィールド名とその値を表示
%#v 値のGo構文表現
%t bool型の値(trueまたはfalse)を文字列として出力
%T 値の型を表示
%d 10進数
%b 2進数
%o 8進数
%x 16進数(小文字)
%X 16進数(大文字)
%f 浮動小数点数(小数点以下6桁まで)
%e 科学的記数法(小文字)
%E 科学的記数法(大文字)
%s 文字列
%q ダブルクォートで囲まれた文字列
%p ポインタのアドレス

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?