計算機科学のブログ

多国通貨(The Money Example) 学習用テストと回帰テスト(Change) 為替レート、銀行

テスト駆動開発 (Kent Beck(著)、和田 卓人(翻訳)、オーム社)の第Ⅰ部(多国通貨(The Money Example))、第14章(学習用テストと回帰テスト(Change))をJavaではなくGo言語で取り組んでみる。

コード

money_test.go

package money

import "testing"

func TestTimes(t *testing.T) {
	five := NewMoneyDollar(5)
	tests := []struct {
		m int
		d Money
	}{
		{2, NewMoneyDollar(10)},
		{3, NewMoneyDollar(15)},
	}
	for _, test := range tests {
		got := five.Times(test.m)
		want := test.d
		if got != want {
			t.Errorf("%v.Times(%v) got %v, want %v", five, test.m, got, want)
		}
	}
}
func TestEq(t *testing.T) {
	tests := []struct {
		mx, my Money
		want   bool
	}{
		{NewMoneyDollar(5), NewMoneyDollar(5), true},
		{NewMoneyDollar(5), NewMoneyDollar(6), false},
		{NewMoneyFranc(5), NewMoneyDollar(5), false},
	}
	for _, test := range tests {
		got := test.mx.Eq(test.my)
		want := test.want
		if got != want {
			t.Errorf("%v.Eq(%v) got %v, want %v",
				test.mx, test.my, got, want)
		}
	}
}
func TestCurrency(t *testing.T) {
	tests := []struct {
		c string
		m Money
	}{
		{"USD", NewMoneyDollar(1)},
		{"CHF", NewMoneyFranc(1)},
	}
	for _, test := range tests {
		got := test.m.Currency()
		want := test.c
		if got != want {
			t.Errorf("%v.Currency() got %v, want %v",
				test.m, got, want)
		}
	}
}

func TestAdd(t *testing.T) {
	five := NewMoneyDollar(5)
	sum := five.Add(five)
	bank := NewBank()
	got := bank.reduce(sum, "USD")
	want := NewMoneyDollar(10)
	if got.Ne(want) {
		t.Errorf("%v.Add(%v) got %v, want %v", five, five, got, want)
	}
}

func TestAddReturnsSum(t *testing.T) {
	five := NewMoneyDollar(5)
	result := five.Add(five)
	sum := result.(Sum)
	tests := []Money{sum.augend, sum.addend}
	for _, test := range tests {
		if test.Ne(five) {
			t.Errorf("%v.Ne(%v)", test, five)
		}
	}
}

// func TestSliceEq(t *testing.T) {
// 	a := []string{"abc"}
// 	b := []string{"abc"}
// 	if a != b {
// 		t.Errorf("%v == %v want true, got false", a, b)
// 	}
// }

func TestBankReduceSum(t *testing.T) {
	sum := NewSum(NewMoneyDollar(3), NewMoneyDollar(4))
	bank := NewBank()
	result := bank.reduce(sum, "USD")
	seven := NewMoneyDollar(7)
	if result.Ne(seven) {
		t.Errorf("%v.Ne(%v)", result, seven)
	}
}

func TestBankReduceMoney(t *testing.T) {
	bank := NewBank()
	result := bank.reduce(NewMoneyDollar(1), "USD")
	one := NewMoneyDollar(1)
	if result.Ne(one) {
		t.Errorf("%v.Ne(%v)", result, one)
	}
}

func TestBankReduceMoneyDifferentCurrency(t *testing.T) {
	bank := NewBank()
	bank.addRate("CHF", "USD", 2)
	twoFranc := NewMoneyFranc(2)
	got := bank.reduce(twoFranc, "USD")
	want := NewMoneyDollar(1)
	if got.Ne(want) {
		t.Errorf("%v.reduce(%v, \"USD\") got %v, want %v",
			bank, twoFranc, got, want)
	}
}

func TestBankIdentityRate(t *testing.T) {
	bank := NewBank()
	got := bank.rate("USD", "USD")
	want := 1
	if got != want {
		t.Errorf("%v.rate(USD, USD) want %v, got %v", bank, want, got)
	}
}

money.go

package money

import "fmt"

// Expression ...
type Expression interface {
	reduce(Bank, string) Money
}

// Money ...
type Money struct {
	amount   int
	currency string
}

// Sum ...
type Sum struct {
	augend, addend Money
}

func (mx Money) String() string {
	return fmt.Sprintf("%v %v", mx.amount, mx.currency)
}

// NewMoney ...
func NewMoney(amount int, currency string) Money {
	return Money{amount, currency}
}

// reduce ...
func (mx Money) reduce(bank Bank, to string) Money {
	// return mx
	// var rate int
	// if mx.currency == "CHF" && to == "USD" {
	// 	rate = 2
	// } else {
	// 	rate = 1
	// }
	rate := bank.rate(mx.currency, to)
	return NewMoney(mx.amount/rate, to)
}

// Currency ...
func (mx Money) Currency() string {
	return mx.currency
}

// Eq ...
func (mx Money) Eq(my Money) bool {
	return mx.amount == my.amount && mx.currency == my.currency
}

// Ne ...
func (mx Money) Ne(my Money) bool {
	return !mx.Eq(my)
}

// Times ...
func (mx Money) Times(m int) Money {
	return NewMoney(mx.amount*m, mx.currency)
}

// NewMoneyDollar ...
func NewMoneyDollar(amount int) Money {
	return NewMoney(amount, "USD")
}

// NewMoneyFranc ...
func NewMoneyFranc(amount int) Money {
	return NewMoney(amount, "CHF")
}

// Add ...
func (mx Money) Add(my Money) Expression {
	// return NewMoney(mx.amount+my.amount, mx.currency)
	return NewSum(mx, my)
}

// NewSum ...
func NewSum(augend, addend Money) Sum {
	return Sum{augend, addend}
}
func (s Sum) reduce(bank Bank, to string) Money {
	amount := s.augend.amount + s.addend.amount
	return NewMoney(amount, to)
}

// Bank 通貨を換算する
type Bank struct {
	rates map[pair]int
}

// NewBank ...
func NewBank() Bank {
	return Bank{rates: map[pair]int{}}
}
func (b Bank) reduce(source Expression, to string) Money {
	return source.reduce(b, to)
}
func (b Bank) addRate(from, to string, rate int) {
	b.rates[newPair(from, to)] = rate
}
func (b Bank) rate(from, to string) int {
	// if from == "CHF" && to == "USD" {
	// 	return 2
	// }
	// return 1
	if from == to {
		return 1
	}
	return b.rates[newPair(from, to)]
}

type pair struct {
	from, to string
}

func newPair(from, to string) pair {
	return pair{from, to}
}
func (px pair) eq(py pair) bool {
	return px.from == py.from && px.to == py.to
}
func (px pair) hashCode() int {
	return 0
}

入出力結果(Terminal, Zsh)

 % go test
--- FAIL: TestReduceSum (0.00s)
    money_test.go:87: 10 USD.Ne(7 USD)
FAIL
exit status 1
FAIL	money	0.386s
% go test
PASS
ok  	money	0.275s
% go test
PASS
ok  	money	0.303s
% go test
--- FAIL: TestReduceMoney (0.00s)
panic: interface conversion: money.Expression is money.Money, not money.Sum [recovered]
	panic: interface conversion: money.Expression is money.Money, not money.Sum

goroutine 12 [running]:
testing.tRunner.func1.1(0x1124c80, 0xc0000582a0)
	/opt/local/lib/go/src/testing/testing.go:1072 +0x30d
testing.tRunner.func1(0xc000001e00)
	/opt/local/lib/go/src/testing/testing.go:1075 +0x41a
panic(0x1124c80, 0xc0000582a0)
	/opt/local/lib/go/src/runtime/panic.go:969 +0x1b9
money.Bank.reduce(...)
	/.../go/src/money/money.go:92
money.TestReduceMoney(0xc000001e00)
	/.../go/src/money/money_test.go:93 +0x45
testing.tRunner(0xc000001e00, 0x1150170)
	/opt/local/lib/go/src/testing/testing.go:1123 +0xef
created by testing.(*T).Run
	/opt/local/lib/go/src/testing/testing.go:1168 +0x2b3
exit status 2
FAIL	money	0.282s
% go test
PASS
ok  	money	0.221s
%