ymatzki

Note: This document is in progress.

テスト駆動開発

memo

まえがき

第1章 仮実装

第2章 明白な実装

第3章 三角測量

第4章 意図を語るテスト

第5章 あえて原則を破るとき

第6章 テスト不足に気づいたら

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

class Money {
    protected int amount;
    public boolean equals(Object object) {
        Money money = (Money) object;
        return amount == money.amount
            && getClass().equals(money.getClass()); // ここを追加
    }
}

第8章 実装を隠す

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)
    }
}

第9章 歩幅の調整

第10章 テストに聞いてみる

// 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");
    }
}

第11章 不要になったら消す

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));
    // }
}

第12章 設計とメタファー

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);
    }
    ...
}
    @Test
    public void testSimpleAddition(){
        Money reduced = bank.reduce(sum, "USD");
        assertEquals(Money.dollar(10), reduced);
    }

wip…

第13章 実装を導くテスト

第14章 学習用テストと回帰テスト

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

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);
    }
}

まとめ

第11章 不要になったら消す

第12章 設計とメタファー

第13章 実装を導くテスト

第14章 学習用テストと回帰テスト

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

第16章 将来の読み手を考えたテスト

第17章 多国籍通貨の全体ふりかえり

第二部 xUnit

第18章 xUnitへ向かう小さな一歩

第19章 前準備

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 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)

ふりかえり

第20章 後片付け

第21章 数え上げ

第22章 失敗の扱い

第23章 スイートにまとめる

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()) # ★ 変更

ふりかえり

Review