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 構造体、メソッド、インターフェース

Posted at

構造体、メソッド、インターフェース

高さと幅を指定して長方形の周囲を計算するために、いくつかのジオメトリコードが必要だとします。 Perimeter(width float64, height float64)関数を記述できます。 ここで、 float64は 123.45のような浮動小数点数用です。

最初にテストを書く

まずはいつも通りテストを最初に記述します。
%fはfloat64用で、.2は小数点以下2桁を出力することを意味します。

shapes_test.go
package structs

import "testing"

func TestPeriMeter(t *testing.T){
	got := PeriMeter(10.0, 10.0)
	want := 40.0

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

次に関数を作成します。

package structs

func PeriMeter(Height, width float64) float64 {
return 0
}

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

--- FAIL: TestPeriMeter (0.00s)
    shapes_test.go:10: got 0.00 want 40.00

成功するコードを書く

簡単ですが、これで完了です。

shapes.go
func PeriMeter(Height, width float64) float64 {
	return 2 * (Height + width)
}

長方形の面積を返す関数

次に、長方形の面積を返す Area(width, Height float64)と呼ばれる関数を作成しましょう。

最初にテストを書く

テストをかき、実行し失敗することを確認します。

shapes_test.go
func TestArea(t *testing.T) {
    got := Area(12.0, 6.0)
    want := 72.0

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

成功するコードを書く

こちらも簡単ですが、これで完了です。

shapes.go
func Area(Width float64, Height float64) float64 {
    return Width * Height
}

リファクタリング

私たちのコードはその役割を果たしますが、四角形について明示的なものは何も含まれていません。不注意な開発者は、三角形の幅と高さを間違った答えを返すことに気付かずにこれらの関数に提供しようとする場合があります。

RectangleAreaのように、より具体的な名前を関数に付けることができます。より適切なソリューションは、この概念をカプセル化するRectangleと呼ばれる独自の型を定義することです。

structを使用して単純なタイプを作成できます。構造体は、データを保存できるフィールドの名前付きコレクションです。

まずテストにこのような構造体をshapes.goファイルに宣言します。

type Rectangle struct {
    Width float64
    Height float64
}

そしてテストコードをプレーンなfloat64ではなく、Rectangle構造体を使用するように変更します。
ちなみに明示的にフィールドを指定してrectangle := Rectangle{Width: 10.0, Height: 10.0}のように書くこともできます。

func TestPerimeter(t *testing.T) {
    rectangle := Rectangle{10.0, 10.0}
    got := Perimeter(rectangle)
    want := 40.0

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

func TestArea(t *testing.T) {
    rectangle := Rectangle{12.0, 6.0}
    got := Area(rectangle)
    want := 72.0

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

しかし、この変更によりテストがパスしなくなります。

./shapes_test.go:12:19: not enough arguments in call to PeriMeter
        have (Rectangle)
        want (float64, float64)
./shapes_test.go:22:14: not enough arguments in call to Area
        have (Rectangle)
        want (float64, float64)

したがって、関数を以下のように修正します。
これでテストがパスするようになりました。

func PeriMeter(rectangle Rectangle) float64 {
	return 2 * (rectangle.Height + rectangle.Width)
}

func Area(rectangle Rectangle) float64 {
	return rectangle.Height * rectangle.Width
}

サークルのArea関数

次の要件は、サークルのArea関数を記述することです。

最初にテストを書く

先ほどのテストを少し修正しcircleのテストを追記します。

ご覧のとおり、fはgに置き換えられています。fを使用すると、正確な10進数を知るのが難しい場合があります。gを使用すると、エラーメッセージで完全な10進数が表示されます。

func TestArea(t *testing.T){

	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12.0, 6.0}
		got := Area(rectangle)
		want := 72.0
	
		if got != want {
			t.Errorf("got %.2f want %.2f", got , want)
		}
	})

	t.Run("circles", func(t *testing.T) {
		circle := Circle{10}
		got := Area(circle)
		want := 314.1592653589793

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

テストを実行するための最小限のコードを記述

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します。

Circleタイプを定義します。

type Circle struct {
	Radius float64
}

しかし、これは失敗します。

./shapes_test.go:29:15: cannot use circle (variable of type Circle) as Rectangle value in argument to Area

なぜなら、Area関数の引数はRectangleであり、Circle 型の変数を Rectangle 型のパラメータとして使用することはできないからです。

ここで、2つの選択肢があります。

  • 同じ名前の関数を異なるpackagesで宣言することができます。新しいパッケージで Area(Circle)を作成することはできますが、ここではやりすぎだと感じます。

  • 代わりに、新しく定義した型にメソッドを定義できます。

メソッドとは?

れまでは functions のみを記述してきましたが、いくつかのメソッドを使用しています。 t.Errorfを呼び出すときは、t (testing.T)のインスタンスでメソッドErrorfを呼び出しています。

メソッドは、レシーバーを持つ関数です。 メソッド宣言は、識別子(メソッド名)をメソッドにバインドし、メソッドをレシーバーの基本タイプに関連付けます。

まずテストを変更して、代わりにメソッドを呼び出し、次にコードを修正しましょう。

shapes_test.go
func TestArea(t *testing.T) {

    t.Run("rectangles", func(t *testing.T) {
        rectangle := Rectangle{12, 6}
        got := rectangle.Area()
        want := 72.0

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

    t.Run("circles", func(t *testing.T) {
        circle := Circle{10}
        got := circle.Area()
        want := 314.1592653589793

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

}

テストを実行しようとすると、それぞれのメソッドが未定義だと出力されます。

./shapes_test.go:19:19: rectangle.Area undefined (type Rectangle has no field or method Area)
./shapes_test.go:29:16: circle.Area undefined (type Circle has no field or method Area)

そこで、以下のようにRectangle構造体とCircle構造体に紐づいたメソッドを作成します。
func (receiverName ReceiverType) MethodName(args)のように書きます。

shapes.go
func (r Rectangle) Area() float64 {
	return 0
}

func (c Circle) Area() float64 {
	return 0
}

詳しくはこちらを参考に。

テストを実行すると、先ほどのメソッドが未定義というエラーは出力されなくなりました。

    --- FAIL: TestArea/rectangles (0.00s)
        shapes_test.go:23: got 0 want 72
    --- FAIL: TestArea/circles (0.00s)
        shapes_test.go:33: got 0 want 314.1592653589793

次に、return0としている箇所をテストが通るように修正します。
mathをインポートして使用します。

func (r Rectangle) Area() float64 {
	return r.Height * r.Width
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

リファクタリング

次にテストをリファクタリングします。
以下のようにinterfaceを定義します。

Go言語では、インターフェースを満たす(実装する)ために、明示的に「この型はこのインターフェースを実装する」と宣言する必要はありません。代わりに、ある型がインターフェースで定義されたメソッドをすべて持っていれば、その型はそのインターフェースを自動的に満たす(実装する)と見なされます。

type Shape interface {
    Area() float64
}

暗黙的なインターフェースの実装:
Goでは、型が必要なメソッドを持っている限り、その型は自動的にインターフェースを実装しています。特別に「この型はこのインターフェースを実装する」と宣言する必要はありません。

切り離し(Decoupling):
インターフェースを使うと、特定の実装に依存せずにコードを書くことができます。例えば、関数がShapeインターフェースを受け取るようにすると、Rectangle、Circle、他のShapeを実装する型を受け取ることができます。

さて、話がそれましたが以下がリファクタリングしたコードです。
checkArea関数に切り出して、先ほど定義したinterfaceであるShape型のshapeを引数に取っています。

こうすることで、Area関数に紐づいた構造体を引き渡せるようになるため、今回の場合だとrectanglecircleを渡すことができ、共通化することができます。

shapes_test.go
func TestArea(t *testing.T) {
	checkArea := func(t *testing.T, shape Shape, want float64) {
		t.Helper()
		got := shape.Area()
		if got != want {
				t.Errorf("got %g want %g", got, want)
		}
}

	t.Run("rectangles", func(t *testing.T) {
			rectangle := Rectangle{12, 6}
			want := 72.0
			checkArea(t, rectangle, want)
	})

	t.Run("circles", func(t *testing.T) {
			circle := Circle{10}
			want := 314.1592653589793
			checkArea(t, circle, want)
	})
}

さらにリファクタリング

構造体についてある程度理解できたので、「テーブル駆動テスト」を紹介します。
テーブル駆動テストは、同じ方法でテストできるテストケースのリストを作成する場合に役立ちます。

テーブル駆動テストは、以下のような構造を持ちます。

  • テストケースの定義: テストデータと期待される結果を含む構造体のスライスを定義します。
  • テストループ: 各テストケースをループで回し、テストを実行します。

要は、同じ方法でテストケースできる場合、スライスで複数のテストケースを用意して、ループで回すことで簡潔にテストを記述できるということです。

これを今回のケースに使うと以下のようになります。

ここでは構造体の配列であるareaTestsを用意します。これはShapeインターフェース(つまり、今回だとRectangleやCircle)とfloat64のwantを設定します。

shepes_test.go
func TestArea(t *testing.T) {

	areaTests := []struct{
		shape Shape
		want  float64
	}{
		{Rectangle{12, 6}, 72.0},
		{Circle{10}, 314.1592653589793},
	}

	for _, tt := range areaTests{
		got := tt.shape.Area()
		if got != tt.want {
			t.Errorf("got %g want %g", got, tt.want)
		}
	}
}

別の形状を追加してテストすることで、先ほどのテストが果たして機能するのか確認するため、Triangleメソッドを追記してみます。

最初にテストを書く

{Triangle{12, 6}, 36.0},をareaTestsに追加します。
この時点では関数がないためテストは失敗します。

{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
{Triangle{12, 6}, 36.0},

テストを実行するための最小限のコードを記述

テストを実行するため以下を追記します。

shapes.go
type Triangle struct {
	Base 	float64
	Height   float64
}
.
.
.
func (t Triangle) Area() float64 {
	return 0
}

テストは失敗しますが、実行に成功します。

shapes_test.go:29: got 0 want 36

成功させるのに十分なコードを書く

以下のように修正すると、テストがパスします。

func (t Triangle) Area() float64 {
	return (t.Base * t.Height) * 0.5
}

リファクタリング

実装は問題ありませんが、テストでは多少の改善が見込めます。

これを見直すと

{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
{Triangle{12, 6}, 36.0},

すべての数値が何を表しているのかすぐには明確ではなく、テストを簡単に理解できるようにする必要があります。

{shape: Rectangle{Width: 12, Height: 6}, want: 72.0},
{shape: Circle{Radius: 10}, want: 314.1592653589793},
{shape: Triangle{Base: 12, Height: 6}, want: 36.0},

Tips

  • エラーメッセージを %#v got %.2f want %.2fに変更できます。 %#v形式の文字列は、フィールドの値を含む構造体を出力するため、開発者はテストされているプロパティを一目で確認できます。

  • テストケースを読みやすくするために、wantフィールドの名前をhasAreaのようなわかりやすい名前に変更できます。

  • テーブル駆動テストの最後のヒントは、t.Runを使用してテストケースに名前を付けることです。

  • 各ケースをt.Runでラップすることで、ケースの名前が出力されるため、失敗時のテスト出力がより明確になります。

上記を踏まえてさらにリファクタリングした結果が以下です。

func TestArea(t *testing.T) {

    areaTests := []struct {
        name    string
        shape   Shape
        hasArea float64
    }{
        {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0},
        {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793},
        {name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0},
    }

    for _, tt := range areaTests {
        // using tt.name from the case to use it as the `t.Run` test name
        t.Run(tt.name, func(t *testing.T) {
            got := tt.shape.Area()
            if got != tt.hasArea {
                t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea)
            }
        })
    }
}
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?