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