Note: This document is in progress.
テスト駆動開発のゴールは「動作するきれいなコード」。
assertTrue(new Dollar(5).equals(new Dollar(5)))
assertFalse(new Dollar(5).equals(new Dollar(6)))
Moneyクラスを作りDollarとFrancはMoneyクラスを継承させるようにした。
class Money {
protected int amount;
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
&& getClass().equals(money.getClass()); // ここを追加
}
}
class Franc extends Money {
Fran(int amount) {
this.amount = amount
}
Money times(int multiplier) {
return new Franc(amount * multiplier)
}
}
class Dollar extends Money {
Fran(int amount) {
this.amount = amount
}
Money times(int multiplier) {
return new Franc(amount * multiplier)
}
}
USD
やCHF
を返すだけUSD
やCHF
をMoneyのFactory Methodであるdollarメソッドやfrancメソッドに移動し、2つサブクラスのコンストラクタを一致させ共通実装に導く。class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}
Money times(int multiplier) {
// return Money.franc(amount * multiplier); // ★ 再度もどす
return new Franc(amount * multiplier, "CHF");
}
}
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}
Money times(int multiplier) {
// return Money.franc(amount * multiplier); // ★ 再度もどす
return new Franc(amount * multiplier, currency);
}
}
// abstract class Money { // ★ 具象クラスにする
class Money {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount; this.currency = currency;
}
// abstract Money times(int multiplier); Money times(int multiplier) { // ★ 具象クラスにする
Money times(int multiplier); Money times(int multiplier) {
return null;
}
String currency() { return currency;
}
public boolean equals(Object object) {
Money money = (Money) object; return amount == money.amount
&& getClass().equals(money.getClass());
}
static Money dollar(int amount) {
return new Dollar(amount, "USD");
}
static Money franc(int amount) {
return new Franc(amount, "CHF");
}
}
class Money {
...
static Money dollar(int amount){
return new Dollar(amount, "USD");
}
static Money franc(int amount){
// FrancではなくMoneyをnewするように変更
// return new Franc(amount, "CHF");
return new Money(amount, "CHF");
}
}
class Money {
...
static Money dollar(int amount){
// DollarではなくMoneyをnewするように変更
// return new Dollar(amount, "USD");
return new Money(amount, "USD");
}
static Money franc(int amount){
return new Money(amount, "CHF");
}
}
@Test
public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
}
@Test
public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
// 上記と内容が重複しているので削除する
// assertTrue(Money.franc(5).equals(Money.franc(5)));
// assertFalse(Money.franc(5).equals(Money.franc(6)));
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}
public class MoneyTest{
...
// 等価性比較は別のテストで担保されているので削除
// @Test
// public void testDifferentClassEquality() {
// assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
// }
}
// Francクラスも不要になったので削除する
// package money;
// class Franc extends Money {
// Franc(int amount, String currency) {
// super(amount, currency);
// }
// }
public class MoneyTest {
// testMultiplicationで担保できているので削除
// @Test
// public void testFrancMultiplication() {
// Money five = Money.franc(5);
// assertEquals(Money.franc(10), five.times(2));
// assertEquals(Money.franc(15), five.times(3));
// }
}
$5 + $5 = $10
の足し算を追加する。public class MoneyTest{
...
@Test
public boid testSimpleAddition(){
Money sum = Money.dollar(5).plus(Money.dollar(5))
assertEquals(Money.dollar(10),sum);
}
}
class Money {
protected int amount;
protected String currency;
Money(int amount, String currency){
this.amount = amount;
this.currency = currency;
}
Money times(int multiplier){
return new Money(amount * multiplier, currency);
}
Money plus(Money addend){
return new Money(amount + addend.amount, currency);
}
...
}
(2+3) * 5
のようなもの
($2+3CHF) * 5
など @Test
public void testSimpleAddition(){
assertEquals(Money.dollar(10), reduced);
}
@Test
public void testSimpleAddition(){
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
wip…
$5 + 10CHF
のテストをかける段階までやってきた。
public class MoneyTest {
...
@Test
public void testMixedAddition() {
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.fran(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}
}
@Test
public void testMixedAddition() {
// ★ 一旦ExpressionをMoneyにする
// Expression fiveBucks = Money.dollar(5);
// Expression tenFrancs = Money.fran(10);
Money fiveBucks = Money.dollar(5);
Money tenFrancs = Money.fran(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}
public Money reduce(Bank bank, String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}
class Sum implements Expression {
Money augend;
Money addend;
Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}
public Money reduce(Bank bank, String to){
int amount = augend.reduce(bank, to).amount + addend.reduce(bank, to). amount;
return new Money(amount, to);
}
}
class Sum implements Expression {
// ★ MoneyをExpressionに変更する
// Money augend;
// Money addend;
// Sum(Money augend, Money addend) {
Expression augend;
Expression addend;
Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}
public Money reduce(Bank bank, String to){
int amount = augend.reduce(bank, to).amount + addend.reduce(bank, to). amount;
return new Money(amount, to);
}
}
class Money implements Expression {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// ★ MoneyをExpressionに変更する
// Money times(int multiplier){
Expression times(int multiplier){
return new Money(amount * multiplier, currency);
}
// ★ MoneyをExpressionに変更する
// Expression plus(Money addend){
Expression plus(Expression addend){
return new Sum(this, addend);
}
public Money reduce(Bank bank, String to) {
int rate = bank.rate(currency, to);
return new Money(amount / rate, to);
}
...
}
@Test
public void testMixedAddition() {
// ★ MoneyをExpressionにするが、fiveBucksはコンパイルエラーになってしまう
// Money fiveBucks = Money.dollar(5);
// Money tenFrancs = Money.fran(10);
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.fran(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}
interface Expression {
Expression plus(Expression addend);
Money reduce(Bank bank, String to);
}
class Money implements Expression {
protected int amount;
protected String currency;
Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
Expression times(int multiplier){
return new Money(amount * multiplier, currency);
}
// ★ publicにする
public Expression plus(Expression addend){
return new Sum(this, addend);
}
public Money reduce(Bank bank, String to) {
int rate = bank.rate(currency, to);
return new Money(amount / rate, to);
}
...
}
class Sum implements Expression {
Expression augend;
Expression addend;
Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}
// ★ 空実装で追加
public Expression plus(Expression addend) {
return null;
}
public Money reduce(Bank bank, String to){
int amount = augend.reduce(bank, to).amount + addend.reduce(bank, to). amount;
return new Money(amount, to);
}
}
まとめ
class TestCaseTest(TestCase):
def testRunning(self):
test = WasRun("testMethod")
assert(not test.wasRun)
test.run()
assert(test.wasRun)
# ★ testSetUpを追加する
def testSetUp(self):
test = WasRun("testMethod")
test.run()
assert(test.wasSetUp)
TestCaseTest("testRunning").run()
TestCaseTest("testSetUp").run() # ★ 追加する
class WasRun(TestCase):
def __init__(self, name):
self.wasRun = None
super().__init__(name)
# ★ setUpを追加する
def setUp(self):
self.wasSetUp = 1
def testMethod(self):
self.wasRun = 1
class TestCase:
def __init__(self, name):
self.name = name
# ★ setUpを追加する
def setUp(self):
pass
def run(self):
self.setUp() # ★ 追加する
method = getattr(self, self.name)
method()
class WasRun(TestCase):
# ★ 削除する
# def __init__(self, name):
# self.wasRun = None
# super().__init__(name)
def setUp(self):
self.wasSetUp = 1
def testMethod(self):
self.wasRun = 1
class TestCaseTest(TestCase):
def testRunning(self):
test = WasRun("testMethod")
# assert(not test.wasRun) # ★ 削除する
test.run()
assert(test.wasRun)
def testSetUp(self):
test = WasRun("testMethod")
test.run()
assert(test.wasSetUp)
class TestCaseTest(TestCase):
# ★ フィクスチャーを追加
def setUp(self):
self.test = WasRun("testMethod")
def testRunning(self):
# test = WasRun("testMethod") # ★ フィクスチャーが担うので削除
self.test.run() # ★ selfを追加
assert(self.test.wasRun) # ★ selfを追加
def testSetUp(self):
# test = WasRun("testMethod") # ★ フィクスチャーが担うので削除
self.test.run() # ★ selfを追加
assert(self.test.wasSetUp) # ★ selfを追加
ふりかえり
print
とrun().summary()
が重複している。class TestCaseTest(TestCase):
...
# ★ 追加
def testSite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
result = suite.run()
assert("2 run, 1 failed" == result.summary())
...
# ★ 追加
print(TestCaseTest("testSuite").run().summary())
class TestSuite:
def __init__(self):
self.tests = []
def add(self, test):
self.tests.append(test)
class TestSuite:
def __init__(self):
self.tests = []
def add(self, test):
self.tests.append(test)
# ★ 追加
def run(self):
result = TestResult()
for test in self.test:
test.run(result)
return result
class TestCaseTest(TestCase):
def testSite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
# result = suite.run() # ★ 削除
result = TestResult() # ★ 追加
suite.run(result) # ★ 追加
assert("2 run, 1 failed" == result.summary())
class TestSuite:
# def run(self): # ★ resultを追加する形に変更
def run(self, result):
# result = TestResult() # ★ 削除
for test in self.test:
test.run(result)
# return result # ★ 削除
class TestCase:
...
# def run(self): # ★ resultを追加する形に変更
def run(self, result):
# result = TestResult() # ★ 削除
result.testStarted()
self.setUp()
try:
method = getattr(self, self.name)
method()
except:
result.testFailed()
self.tearDown()
# return result # ★ 削除
# ★ 削除
# print(TestCaseTest("testTemplateMethod").run().summary())
# print(TestCaseTest("testResult").run().summary())
# print(TestCaseTest("testFailedResult").run().summary())
# print(TestCaseTest("testFailedResultFormatting").run().summary())
# print(TestCaseTest("testSuite").run().summary())
# ★ 追加
suite = TestSuite()
suite.add(TestCaseTest("testTemplateMethod"))
suite.add(TestCaseTest("testResult"))
suite.add(TestCaseTest("testFailedResult"))
suite.add(TestCaseTest("testFailedResultFormatting"))
suite.add(TestCaseTest("testSuite"))
result = TestResult()
suite.run(result)
print(result.summary())
class TestCaseTest(TestCase):
def testTemplateMethod(self):
test = WasRun("testMethod")
# test.run() # ★ 削除
result = TestResult() # ★ 追加
test.run(result) # ★ 追加
assert("setUp testMethod tearDown " == test.log)
def testResult(self):
test = WasRun("testMethod")
# result = test.run() # ★ 削除
result = TestResult() # ★ 追加
test.run(result) # ★ 追加
assert("1 run, 0 failed" == result.summary())
def testFailedResult(self):
test = WasRun("testBrokenMethod")
# result = test.run() # ★ 削除
result = TestResult() # ★ 追加
test.run(result) # ★ 追加
assert("1 run, 1 failed" == result.summary())
class TestCaseTest(TestCase):
# ★ 追加
def setUp(self):
self.result = TestResult()
def testTemplateMethod(self):
test = WasRun("testMethod")
# result = TestResult() # ★ 削除
test.run(self.result) # ★ 変更
assert("setUp testMethod tearDown " == test.log)
def testResult(self):
test = WasRun("testMethod")
# result = TestResult() # ★ 削除
test.run(self.result) # ★ 変更
assert("1 run, 0 failed" == self.result.summary()) # ★ 変更
def testFailedResult(self):
test = WasRun("testBrokenMethod")
# result = TestResult() # ★ 削除
test.run(self.result)
assert("1 run, 1 failed" == self.result.summary()) # ★ 変更
def testFailedResultFormatting(self):
# result = TestResult() # ★ 削除
self.result.testStarted() # ★ 変更
self.result.testFailed() # ★ 変更
assert("1 run, 1 failed" == self.result.summary()) # ★ 変更
def testSite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
# result = TestResult() # ★ 削除
suite.run(self.result) # ★ 変更
assert("2 run, 1 failed" == self.result.summary()) # ★ 変更
ふりかえり