LoginSignup
3
1

More than 1 year has passed since last update.

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

Posted at

この記事の内容

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

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

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

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

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

public/privateとテストのpackate

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

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

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

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

継承をどうするか

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

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

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

type Money struct {
    amount int64
}

type Dollar struct {
    Money
}

特に困ったのはFrancとDollarを比較する部分です。2

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

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

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

通貨の比較は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()
}
  • equals の一般化
  • 改めて equals の一般化 NEW

構造体の比較

MoneyからExpressionへ一般化するため、Timesメソッドの戻り値を置き換えたとき、テストがレッドになりました。3

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では異なる結果となりました。

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

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

- 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の特徴がよく見えたと思います。
マニュアルやチュートリアルをやっているだけでは得られない学びがあるので、複数の言語での実装を試してみてはいかがでしょうか。

  1. https://shop.ohmsha.co.jp/shopdetail/000000004967/

  2. 第7章 疑念をテストに翻訳する

  3. 第15章 テスト任せとコンパイラ任せ

3
1
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
3
1