2023/08/20

golang: interfaceの勉強 (3)

続き。

hiro99ma blog: golang: interfaceの勉強 (2)
https://blog.hirokuma.work/2023/08/golang-interface-2.html


struct と duck typing

golang の調べ物をしたとき、duck typing という言葉を見ることがある。
メソッド名・引数・戻り値が一致したら同じものと思われる、くらいに思っていたのでこういうことができると考えていた。

package main

import "fmt"

type Abc struct{}

func (a *Abc) Greet() {
    fmt.Println("Hello")
}

type Def struct{}

func (d *Def) Greet() {
    fmt.Println("Aloha")
}

func main() {
    abc := new(Abc)
    abc.Greet()
    abc = new(Def)
    abc.Greet()
}

これは赤文字のところでコンパイルエラーになる。コンパイルエラーというか文法エラーというか。

interface Ghi を作ってそこに Greet() を書いておき、使う場合は Ghi の変数として扱ってやると、Abc と Def が Ghi の派生であるとかを匂わせる必要もなく動くようになる。

package main

import "fmt"

type Ghi interface {
    Greet()
}

type Abc struct{}

func (a *Abc) Greet() {
    fmt.Println("Hello")
}

type Def struct{}

func (d *Def) Greet() {
    fmt.Println("Aloha")
}

func main() {
    var abc Ghi
    abc = new(Abc)
    abc.Greet()
    abc = new(Def)
    abc.Greet()
}

struct と interface を関連付けるような記述をしないというところが「プログラミング言語Go」のインターフェースの章で『暗黙的に満足される』といっている意味なんだろう。

暗黙的だと関連性がわからなくて悩みそうだと思ったなら、明示的に書いてもよいだろう。

type Abc struct{ Ghi }

明示的に interface があることを書いておきながらも、interface の方に定義されたメソッドを書いていない場合はコンパイルエラーになりそうだが、そうはならない。
どうなるかというと、未定義のメソッドを呼び出そうとした時点で実行時エラーになる。

panic: runtime error: invalid memory address or nil pointer dereference

package main

import "fmt"

type Ghi interface {
    Greet()
}

type Abc struct{ Ghi }

// func (a *Abc) Greet() {
//     fmt.Println("Hello")
// }

type Def struct{ Ghi }

func (d *Def) Greet() {
    fmt.Println("Aloha")
}

func main() {
    var abc Ghi
    abc = new(Abc)
    abc.Greet()
    abc = new(Def)
    abc.Greet()
}

goalng は全体的に縛り付けるような決まりが少ない気がする。その辺は C言語っぽいなーと感じる。

ともかく、golang は duck typing な言語なのかもしれないが(厳密な定義はよく知らない)、それは何でもかんでもじゃなくて interface が関係した場合だけなんだと思われる。


テストでinterfaceを使おうとする

mock なのか stub なのか driver なのか厳密なところ避けさせてもらう。
本物ではなく偽物にしたいということで fake という言葉を使おう。

テストで fake を使いたい場合、テストしたい関数なりメソッドなりは変更したくないのだが、その中で実際に行う処理は偽物に置き換えたいということだと思う。
ネットワークにアクセスする処理だがエラーを起こしたことにしてテストしたい、とか、ハードウェアにアクセスする処理だが実機では確認できないのでダミーの処理で置き換えたい、とか。

image

まず、テスト対象の関数/メソッドは、その中から呼び出す次の処理を切替ができるように書いておかないといかん。上の図だと黄色の丸で表しているが、これが golang でいうところの関数だったら切り替えるのは難しいだろう(タグで切り替えるということはできそうだが)。

ではメソッドの中から別のメソッドを呼ぶように書くとして、何をどう書くと良いだろう。

Golangにおけるinterfaceをつかったテストで mock を書く技法 - haya14busa
http://haya14busa.com/golang-how-to-write-mock-of-interface-for-testing/

こちらでは、対象となる方は interface を名前を付けて書いている。テスト対象自体が interface を所持しているのではなく外側にあって(所持してても良いだろうが)、new するときに設定しているのか。そして fake の方では interface を無名の埋め込みにしている。

 

ではもうちょっと簡易なコードにして試そう。
mocktest というディレクトリを作り、その中に main.go,  greet/greet.go, greet/machine.go というファイルを置く。

  • main.go
package main

import (
    "fmt"
    "mocktest/greet"
)

func main() {
    g := greet.NewGreet()
    g.SetData(3)
    fmt.Println(g.GetData())
}
  • greet/greet.go
package greet

type Greet struct {
    m     MachineInterface
    value int32
}

func NewGreet() *Greet {
    g := &Greet{}
    g.m = NewMachineData()
    return g
}

func (g *Greet) GetData() int32 {
    return g.value + g.m.Get()
}

func (g *Greet) SetData(value int32) {
    g.value = value
    g.m.Set(value)
}
  • greet/machine.go
package greet

import (
    "math/rand"
    "time"
)

type MachineInterface interface {
    Get() int32
    Set(value int32)
}

type MachineData struct {
    value int32

    r *rand.Rand
}

func NewMachineData() *MachineData {
    m := &MachineData{}
    m.r = rand.New(rand.NewSource(time.Now().UnixNano()))
    return m
}

func (m *MachineData) Get() int32 {
    multi := m.r.Int31n(100)
    return multi * m.value
}

func (m *MachineData) Set(value int32) {
    m.value = value
}

「go run .」などとして実行すると、毎回異なる値が出力されるだろう。
greet/machine.go が乱数を使っていて、ちょっとそのままではテストしづらいというようにしている。やっている内容として意味は無い。 greet/machine.go が制御できない動作をするということだけわかっていればよい。
ここでテストするならば greet/greet.go のメソッドになるし、fake にしたいのは greet/machine.go になる。

では適当にテストコードを書いてみる。

  • greet/greet_test.go
package greet

import (
    "testing"
)

type fakeMachineData struct {
    MachineInterface

    FakeGetData func() int32
    FakeSetData func(value int32)
}

func (f *fakeMachineData) Get() int32 {
    return f.FakeGetData()
}

func (f *fakeMachineData) Set(value int32) {
    f.FakeSetData(value)
}

func TestGetData1(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 10
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(3)
    value := mo.GetData()
    if value != 13 {
        t.Errorf("GetData: %v", value)
    }
}

func TestGetData2(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 15
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(5)
    value := mo.GetData()
    if value != 20 {
        t.Errorf("GetData: %v", value)
    }
}

$ go test ./greet/ -v
=== RUN   TestGetData1
--- PASS: TestGetData1 (0.00s)
=== RUN   TestGetData2
--- PASS: TestGetData2 (0.00s)
PASS
ok      mocktest/greet  0.001s

greet/greet_test.go は package が greet なので、小文字のフィールドにもアクセスできる。それを利用して greet.NewGreet() で設定している NewMachineData() を上書きしている。

 

greet/machine.go を別パッケージにしてみよう。
外部のライブラリを使っているとそうなるだろう。

  • greet/greet.go
package greet

import "mocktest/machine"

type Greet struct {
    m     machine.MachineInterface
    value int32
}

func NewGreet() *Greet {
    g := &Greet{}
    g.m = machine.NewMachineData()
    return g
}

func (g *Greet) GetData() int32 {
    return g.value + g.m.Get()
}

func (g *Greet) SetData(value int32) {
    g.value = value
    g.m.Set(value)
}
  • machine/machine.go
package machine

import (
    "math/rand"
    "time"
)

type MachineInterface interface {
    Get() int32
    Set(value int32)
}

type MachineData struct {
    value int32

    r *rand.Rand
}

func NewMachineData() *MachineData {
    m := &MachineData{}
    m.r = rand.New(rand.NewSource(time.Now().UnixNano()))
    return m
}

func (m *MachineData) Get() int32 {
    multi := m.r.Int31n(100)
    return multi * m.value
}

func (m *MachineData) Set(value int32) {
    m.value = value
}
  • greet/greet_test.go
package greet

import (
    "mocktest/machine"
    "testing"
)

type fakeMachineData struct {
    machine.MachineInterface

    FakeGetData func() int32
    FakeSetData func(value int32)
}

func (f *fakeMachineData) Get() int32 {
    return f.FakeGetData()
}

func (f *fakeMachineData) Set(value int32) {
    f.FakeSetData(value)
}

func TestGetData1(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 10
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(3)
    value := mo.GetData()
    if value != 13 {
        t.Errorf("GetData: %v", value)
    }
}

func TestGetData2(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 15
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(5)
    value := mo.GetData()
    if value != 20 {
        t.Errorf("GetData: %v", value)
    }
}

machine を import するようになるくらいの変更しかない。
外部のパッケージだったら interface があっても不自然ではない。

もしここで machine/machine.go に interface がなかったらどうしよう?
その場合は greet/greet.go の方で作っておけば良い。「暗黙的に満足される」から machine/machine.go は変更しなくても良いのだ。
案外、なんとかなるものだ。

  • macihne/machine.go
package machine

import (
    "math/rand"
    "time"
)

// type MachineInterface interface {
//     Get() int32
//     Set(value int32)
// }

type MachineData struct {
    value int32

    r *rand.Rand
}

func NewMachineData() *MachineData {
    m := &MachineData{}
    m.r = rand.New(rand.NewSource(time.Now().UnixNano()))
    return m
}

func (m *MachineData) Get() int32 {
    multi := m.r.Int31n(100)
    return multi * m.value
}

func (m *MachineData) Set(value int32) {
    m.value = value
}
  • greet/greet.go
package greet

import "mocktest/machine"

type MachineInterface interface {
    Get() int32
    Set(value int32)
}

type Greet struct {
    m     MachineInterface
    value int32
}

func NewGreet() *Greet {
    g := &Greet{}
    g.m = machine.NewMachineData()
    return g
}

func (g *Greet) GetData() int32 {
    return g.value + g.m.Get()
}

func (g *Greet) SetData(value int32) {
    g.value = value
    g.m.Set(value)
}
  • greet/greet_test.go
package greet

import (
    "mocktest/machine"
    "testing"
)

type fakeMachineData struct {
    machine.MachineData

    FakeGetData func() int32
    FakeSetData func(value int32)
}

func (f *fakeMachineData) Get() int32 {
    return f.FakeGetData()
}

func (f *fakeMachineData) Set(value int32) {
    f.FakeSetData(value)
}

func TestGetData1(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 10
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(3)
    value := mo.GetData()
    if value != 13 {
        t.Errorf("GetData: %v", value)
    }
}

func TestGetData2(t *testing.T) {
    fakeData := &fakeMachineData{
        FakeGetData: func() int32 {
            return 15
        },
        FakeSetData: func(value int32) {
        },
    }
    mo := NewGreet()
    mo.m = fakeData
    mo.SetData(5)
    value := mo.GetData()
    if value != 20 {
        t.Errorf("GetData: %v", value)
    }
}

0 件のコメント:

コメントを投稿

コメントありがとうございます。
スパムかもしれない、と私が思ったら、
申し訳ないですが勝手に削除することもあります。

注: コメントを投稿できるのは、このブログのメンバーだけです。