GoでTDD本の多国通貨やってみた

# この記事の内容

Go言語の学習のため、『テスト駆動開発』[^1]の第Ⅰ部 多国通貨をやってみました。
言語の違いから書籍のとおりにいかないことはあると思いますが、そういった部分も含めて「Goでどのようにできるのか」を学んでいくという趣旨です。
[^1]:https://shop.ohmsha.co.jp/shopdetail/000000004967/

:::note warn
Go、Javaともにビギナーです。
:::

# `testing`パッケージには`assertXXX`が無い

最初のテストで`assertEquals`の代替を探すことになったのですが、`testing`パッケージにはそのようなものはありませんでした。

https://qiita.com/Jxck_/items/8717a5982547cfa54ebc

FAQにも目を通しましたが、「エラーメッセージをきちんと書こう」とか「必要なものはすでにある」というのが理由だそうです。
なるほどと思いつつも「一つのアサーションで３行になるのは長いな」と思うこともありました。そのあたりはテストを工夫したりヘルパーを用意するなどが必要になるでしょう。

# public/privateとテストのpackate

Goでは「メソッド名の先頭の文字が大文字か小文字か」でpublic/privateが決まります。
しかし私は本来publicにすべきメソッドがprivateであることにしばらく気が付きませんでした。もちろん慣れの問題もあるのですが、大きな原因はテストとプロダクトが同じパッケージだったからです。

Goでは同じパッケージ同士であればprivateにアクセスできますが、私の経験では通常の手段でテストからprivateメソッドにアクセスすることは出来なかったので、これは新しい経験です。
privateメソッドを直接テストするケースは少ないので、この点についてどう考えるべきか調べてみました。

いくつかのフレームワークを見てみましたが、一部のテストは`xxx_test`というパッケージになっていました。どうやら使い分けをしているようです。

そして、同様の疑問についての回答をみつけました。
「ブラックボックステストをするかホワイトボックステストするか」で考えるのが良さそうです。

https://stackoverflow.com/questions/19998250/proper-package-naming-for-testing-with-the-go-language

# 継承をどうするか

Goにはクラスが無いので当然その継承もありません。`Dollar`クラスや`Franc`クラスから`Money`クラスに移行していく過程は悩みどころが多い部分でした。

まずクラスは構造体、継承は構造体の埋め込みとインターフェースで対応しました。

```go:大雑把な構造
type MoneyInterface interface {
    Amount() int64
    Equals(MoneyInterface) bool
}

type Money struct {
    amount int64
}

type Dollar struct {
    Money
}
```

特に困ったのはFrancとDollarを比較する部分です。[^2]
[^2]:第７章 疑念をテストに翻訳する

```java
public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount
        && getClass().equals(money.getClass());
}
```

この`getClass()`にどう対応したものかと`reflect`パッケージや型アサーションなどを模索したのですが、「これは言語の違いでどうしようもない」という結論になりました。
（もともとは歩幅を小さくしてこのような過程を踏んでいるので頑張ってやるものでも無いのですが、最初に示したようにGoを理解するためなので何か手段は無いのか探りました）

具体的には、直前の章で一般化した`Equals`を次のようにしました。

```go:通貨の比較はDollarとFrancそれぞれで、amountはMoneyで
// Dollar
func (d Dollar) Equals(mi MoneyInterface) bool {
    return d.Money.Equals(mi) &&
        reflect.TypeOf(d).Name() == reflect.TypeOf(mi).Name()
}

// Franc
func (f Franc) Equals(mi MoneyInterface) bool {
    return f.Money.Equals(mi) &&
        reflect.TypeOf(f).Name() == reflect.TypeOf(mi).Name()
}

// Money
func (m Money) Equals(mi MoneyInterface) bool {
    return m.amount == mi.Amount()
}
```

- [x] equals の一般化
- [ ] 改めて equals の一般化 **NEW**

# 構造体の比較

`Money`から`Expression`へ一般化するため、`Times`メソッドの戻り値を置き換えたとき、テストがレッドになりました。[^3]
[^3]:第１５章 テスト任せとコンパイラ任せ

```diff_go
type Expression interface {
    Reduce(Bank, string) Money
}

- func (m Money) Times(multiplier int64) Money {
+ func (m Money) Times(multiplier int64) Expression {
    return Money{m.amount * multiplier, m.currency}
}

func TestMultiplication(t *testing.T) {
    five := NewDollar(5)
    ten := NewDollar(10)
    if product := five.Times(2); !ten.Equals(product) {
        t.Fatalf(`amount is not equal. expect: 10, actual: %d`, product.Amount())
    }
}
```

`Expression`インターフェースには`Equals`メソッド無いためです。JavaではObjectクラスに実装されているため問題なく実行できるのでしょうが、Goでは異なる結果となりました。

ここで書籍の流れに沿うためにあえて目をそらしていた構造体の比較に注目します。

https://qiita.com/Sekky0905/items/1ff4979d80b163e0aeb6

Goでは構造体の各フィールドが同じ値であれば`==`で等価と判断されるようです。現状ではそれで十分なので、`Equals`メソッドを廃止することにしました。

```diff_go
- func (m Money) Equals(money Money) bool {
-     return m.amount == money.amount &&
-         m.currency == money.currency
- }

func TestMultiplication(t *testing.T) {
    five := NewDollar(5)
    ten := NewDollar(10)
-   if product := five.Times(2); !ten.Equals(product) {
+   if product := five.Times(2); product != NewDollar(10) {
        t.Fatalf(`amount is not equal. expect: 10, actual: %d`, product.Amount())
    }
}
```

# まとめ

異なる言語で実装されたものを「Goでどのように実装するか」という形でやってみたところ、Goの特徴がよく見えたと思います。
マニュアルやチュートリアルをやっているだけでは得られない学びがあるので、複数の言語での実装を試してみてはいかがでしょうか。