`
airu
  • 浏览: 267480 次
  • 性别: Icon_minigender_1
  • 来自: 云南
社区版块
存档分类
最新评论

TDD 学习笔记(三)

 
阅读更多
接上一篇。这里到了比较关键的时候了。
Kent跨大步了,但是如何找到合适的步伐,还是需要不断从小步尝试。
现在看看目前的to-do lists吧。
  • 5美元 + 5美元 = 10 美元
  • 5美元 + 10法郎 = 10 美元 假设美元对法兰的汇率是 1:2


这里要注意的是两件事,首先,加法的引入,然后,汇率实现。

我们需要快速在脑子里浮现出一个场景。我们去银行兑换货币,银行提供汇率……

这里,先实现一个简单的加法测试

@Test public void  testSimpleAddition(){
    Money sum = Money.dollar(5).plus(Money.dollar(5));
    assertEquals(Money.dollar(10),sum);
}


很简单,现在来实现已经是轻车熟路了吧。

Money
public Money plus(Money addend){
    new Money(amount + addend.amount,currency);
} 

这里算是一眼就看出来的,所以没必要返回一个 Money.dollar(10)了。测试,OK。
接下来要考虑的,就是如何比较不同的货币,也就是货币转换。首先要有银行,然后呢不同的货币如何表示在一个类中?这里需要在抽象一层。
作者在书中使用了表达式, 5美元+10法郎 这就是一个表达式。我们通过银行,来把这个表达式兑换成美元,或者法郎(根据汇率)。


@Test public void  testSimpleAddition(){
    Moeny five = Money.dollar(5);
    Expression sum = five.plus(five);
    Bank bank = new Bank();
    Moeny result = bank.reduce(sum,"USD");
    assertEquals(Money.dollar(10),sum);  
}


这就是我们脑子中浮现的一个样子。好了,接下来就一个一个实现。
首先修改plus方法。
Money 类
public Expression plus(Money addend){
    new Money(amount+addend.amount,currency);
}


Expression 接口
public interface Expression{

}


Bank  类
public class Bank{
public Money reduce(Expression expression,String to){
    return new Money.dollar(10);//just fake implementation
}
}

测试一下,OK。没什么问题。但是还有fake implementation。我们仔细分析下,发现其实Expression接口与Money还是有区别的,所以,在plus的时候,应该返回一个真正的实现了Expression 的类。这里就叫做Sum吧。这样,Sum应该又有一个加数和一个被加数。
测试上:
@Test public void testSum(){
   Moeny five = Money.dollar(5);
   Expression result = five.plus(five);
   Sum sum = (Sum)result;
   assertEquals(sum.augend,five);
   assertEquals(sum.addend,five);  
}

Sum
public class Sum implements Expression{
private Money augend;
private Money addend;
public Sum(Money augend, Money addend){
    this.augend = augend;
    this.addend = addend;
}
}


Money 类中的plus可以返回一个真正的Expression类了。
public Expression plus(Money addend){
   return new Sum(this,addend);
}


这是,我们的问题终于回到这个fake implementation,让我们再写个测试,来暴露问题。
@Test public void  testReduceSum(){
    Expression sum = Money.dollar(3).plus(Money.dollar(4));
    Bank bank = new Bank();
    Moeny result = bank.reduce(sum,"USD");
    assertEquals(Money.dollar(7),sum);  
}


public class Bank{
public Money reduce(Expression expression,String to){
    Sum sum = (Sum)expression;
    return new Money(sum.augend.amount+sum.addend.amount,to);   
}
}

这下是通过测试了,但是我们看到了又臭又长的一串串 sum.augend.amount 之类的。
这样的东西出现,预示着我们应该使用Move method重构。于是把reduce方法移到Sum中。
public class Sum implements Expression{
private Money augend;
private Money addend;
public Sum(Money augend, Money addend){
    this.augend = augend;
    this.addend = addend;
}
public Money reduce(String to){
    return new Money(augend.amount + addend.amount,to);
}
}

于是Bank的reduce可以这样写

public class Bank{
public Money reduce(Expression expression,String to){
    Sum sum = (Sum)expression;
    return sum.reduce(to);
}
}

这下简洁多了。既然多了个函数,我们为此写个测试吧。

@Test public void testReduceMoney(){
    Money money = Money.dollar(1);
    Bank bank = new Bank();
    Money m = bank.reduce(money,"USD");
    assertEquals(Money.dollar(1),m);
}

这个测试很有意思,既然Money是一种表达式,那么就直接放到bank.reduce里检验。
结果问题出来了。造型错误。我们还得修改bank.reduce
public class Bank{
public Money reduce(Expression expression,String to){
    if(expression instanceof Money) return (Money)expression;
    Sum sum = (Sum)expression;
    return sum.reduce(to);
}
}

这下代码越来越难看了。这完全背离了我们的意图。不过通过这里,我们很快就能发现,Money和Sum如果都有一致的方法,就很好了。于是Expression接口多了reduce方法

public interface Expression{
    public Money reduce(String to);
}


Money需要添加此方法。(Money类实现了Expression接口)。

public Money reduce(String to){
    return this;
}


现在,Bank的reduce方法终于可以这样写了
public Money reduce(Expression expression, String to){
    return expression.reduce(to);
}

清晰很多了。测试通过,接下来是to-do lists的第二项未实现的。那就是;
5美元 + 10法郎 = 10 美元。

在写测试之前,我们一般要考虑一下如何实现,这里,我们的银行应该提供一个汇率。
这样,我们就可以转换不同币种了。
我们可以这样写测试:

@Test public void testRecuceMoneyDifferentCurrency(){
    Bank bank = new Bank();
    bank.addRate("CHF","USD",2)
    Money result = bank.reduce(Money.franc(10),"USD");
    assertEquals(Money.dollar(5),result);
}


这是对一个基本的汇率转换的测试。我们暂时还没有一个清晰的方法来实现汇率。于是就使用triangulation吧。先做一个stub实现。具体分析代码,最后实现转换是在Money中的reduce方法。
Money 类
public Money reduce(String to){
    int rate = currency.equals("CHF")&&to.equals("USD")?2:1;
    return new Money(amount/rate,to);
}

显然这是为了通过测试。
新增一个测试 :
@Test public void testBankRate(){
    Bank bank = new Bank();
    bank.addRate("USD","CHF",2); // reverse the rate
    Money result = bank.reduce(Money.dollar(10),"CHF");
    assertEquals(Money.franc(5),result);
}

显然,我们的银行没有起到作用。我们可以应该修改接口Expression中的reduce方法

public interface Expression{
    public Money reduce(Bank bank, String to);
}

于是接着改Money

public Money reduce(Bank bank, String to){
    int rate = (currency.equals("CHF"))&&(to.equals("USD"))?2:1;
    return new Money(amount/rate,to);

}

当然Sum的也要改。这就省略。
rate应该由Bank提供。
所以Extract Method到Bank类。

Bank
public int rate(){
    return (currency.equals("CHF"))&&(to.equals("USD"))?2:1;
}


Money
public Money reduce(Bank bank, String to){
    int rate = bank.rate();
    return new Money(amount/rate, to);
}


测试,还是不能通过。因为那个判断其实是个fake implementation,所以需要真正考虑如何实现了。这里,脑海里第一浮现的就是Collection类。作者使用Hashtable来存放一个key/value的键值对。这样每个转换都对应一个汇率。
修改Bank类的rate方法,添加参数。
Bank类。
private Hashtable rates;
public void addRate(String from ,String to, int rate){
    rates.put(new Pair(from,to),new Ineger(rate));
}
public int rate(String from ,String to){
    Pair key = new Pair(from,to);
    return ((Integer)rates.get(key)).intValue();
}


这里有引入一个新类,作为key的封装。我们有需要测试了。

@Test public void testkeyEquality(){
    assertEquals(new Pair("USD","CHF"),new Pair("USD","CHF"));
    assertFalse((new Pair("USD","CHF")).equals(null));
}


如果你还记得前面的货币比较的话,这里应该容易理解,我们马上知道该如何比较Pair

public class Pair{
private String from;
private String to;
public Pair(Stirng from, String to){
    this.from = from;
    this.to = to;
}

@Override
public boolean equals(Object obj){
    if(obj == null) return false;
    if(obj == this) return true;
    if(obj instanceof Pair){
        Pair newpair = (Pair)obj;
        return (from.equals(obj.from) && (to.equals(obj.to));
    }else return false;
}

@Override
public int hashCode(){
    return 0;
}


这里的euqals方法其实做过了。因为我们的测试没有放映到我需要这么做。
对于hashCode方法呢,暂时返回0,使之退化成线性查找,我们这里但求通过测试。
经过这番折腾,测试终于通过了。我们这里又想到一个测试:

@Test public void testIdentityRate(){
    assertEquals(1,(new Bank()).rate("USD","USD"));
}

这类测试属于边界测试吧。往往起到意想不到的作用。果然 ,我们又一次倒下了。
对于这类一般性问题,我们在rate里面没有考虑全面。
public int rate(String from ,String to){
    if(from.equals(to)) return 1;
    Pair key = new Pair(from,to);
    return ((Integer)rates.get(key)).intValue();
}

加上这个,就算完成rate的测试了吧。因为我们马上就要实现 5美元+10法郎=10美元的测试了。

@Test public void testMixedAddition(){
    Money five = Money.dollar(5);
    Expression sum = five.plus(Money.franc(10));
    Bank bank = new Bank();
    bank.addRate("CHF","USD",2);
    Money result = bank.reduce(sum,"USD");
    assertEquals(Money.dollar(10),result);
}

这个测试失败了。我们应该得到10美元,但是,我们得到的是15美元。问题出在Sum类。
public Money reduce(String to){
    return new Money(augend.amount + addend.amount,to);
}

一下就明白了,不同的表达式需要经过reduce,才能相加。
public Money reduce(Bank bank,String to){
    return new Money(augend.reduce(bank,to)+ addend.reduce(bank,to),to);
}

好了,现在工作了。但是我们看到,Sum中的加数与被加数同时也应该是表达式。于是
可以再测试通过的情况下,重构,把Sum变为:

public class Sum implements Expression{
private Expression augend;
private Expression addend;
public 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);
}
}

既然都是面向接口了,注意Money中的plus也要改改。times也该改改
public Expression plus(Expression addend){
   return new Sum(this,addend);
}
public Expression times(int multiplier){   
    return new Money(amount*multiplier,currency);
}


然后相应的测试代码也改改:
@Test public void testMixedAddition(){
    Expression five = Money.dollar(5);
    Expression sum = five.plus(Money.franc(10));
    Bank bank = new Bank();
    bank.addRate("CHF","USD",2);
    Money result = bank.reduce(sum,"USD");
    assertEquals(Money.dollar(10),result);
}

改完以后,测试发现,Expression接口么有plus方法,这就很明显了,Expression应该有plus和times接口。加上。

Money的实现未变。
Sum的plus实现呢?暂且未知。
public Expression plus(Expression addend){
    return null;
}

于是测试通过了。我们需要一个新的测试Sum的plus,来发现我们的伪实现。
@Test public void testSumPlus(){
    Expression five = Money.dollar(5);
    Expression sum = five.plus(Money.franc(10));
    Bank bank = new Bank();
    bank.addRate("CHF","USD",2);
    Money result = bank.reduce(sum.plus(Money.franc(20),"USD");
    assertEquals(Money.dollar(20),result);
}


现在实现Sum的plus方法,其实很简单。
public Expression plus(Expression expression){
    return new Sum(this,expression);
}

测试通过。就这么简单。现在我们可能会在to-do lists 中加入,

(5美元+10法郎)*3 = 30 美元

测试代码:
@Test public void testSumPlus(){
    Expression fiveBuck = Money.dollar(5);
    Expression tenFranc= Money.franc(10);
    Bank bank = new Bank();
    bank.addRate("CHF","USD",2);
    Money result = bank.reduce(new Sum(fiveBuck,tenFranc).times(2),"USD");
    assertEquals(Money.dollar(20),result);
}




我们的times方法,同样需要对Expression方法有效。
Sum中的实现
public Expression times(int multiplier){
    return new Sum(augend.times(multiplier),addend.times(multiplier));
} 

Ok ,测试通过。写到这里,基本就告一个段落了,但是这里远远没有完。
只是我们的to-do lists空了,我们还可以添加,继续重构代码。
0
0
分享到:
评论

相关推荐

    Study__TDD:个人TDD学习资料库

    埃迪的TDD学习笔记 :police_car_light: 警告不要合并任何PR。 :memo: 内容所有摘要都放在“问题”选项卡中。 当前,列出了以下内容: NHN FE개발랩摘要-Finsihed 견고한JS소프트웨어만들기-进行中测试Vue.js应用程序...

    AppFuse学习笔记(J2EE入门级框架)

    采用TDD的开发方式,使用JUnit测试各层,甚至测试 jsp 输出的 w/o 错误。为了简化开发,预定义好了一套目录结构、基类、用来创建数据库、配置Tomcat、测试部署应用的 Ant 任务,帮助快速自动生成源程序和自动维护...

    appfuse 学习笔记

    采用TDD的开发方式,使用JUnit测试各层,甚至测试 jsp 输出的 w/o 错误。为了简化开发,预定义好了一套目录结构、基类、用来创建数据库、配置Tomcat、测试部署应用的 Ant 任务,帮助快速自动生成源程序和自动维护...

    tdd-rails-pluralsight:使用RSpec,Capybara和Cucumber以及Pluralsight课程学习Rails的TDD

    具有RSpec,Capybara和Cucumber的测试驱动Rails 我在TDD Rails上的Pluralsight 课程中的笔记。 常用命令命令描述bin/rails s 启动Rails服务器bin/rails c 启动Rails控制台bundle exec rake routes 列出所有路线bin/...

    ng-tdd:了解AngularJS测试驱动开发

    ng-tdd 通过本书学习 它是如何工作的? 安装依赖项 npm install bower install 使用业力运行规格测试 karma start karma.conf.js 使用量角器运行e2e测试 http-server -p 5555 protractor conf.js 我的笔记 第二...

    java银行笔试题-tdd-bank-account-java:TD-银行账户-java

    中要学习/练习的两个关键事项: 测试驱动开发 结对编程 指示 使用以下命令克隆存储库。 git clone https://github.com/xp-dojo/tdd-bank-account-java 如果您遇到 SSL 问题,可以尝试以下操作。 git clone -c ...

    云学习:我对云的个人笔记和笔记

    我的云学习这是一个回购记录,用于跟踪我在软件开发(尤其是云计算)方面学到的东西。目的我决定这样做是为了让自己有更新的地方,以防万一我忘记了自己学到的东西。 另外,我决定将它公开给其他人学习,使用和重用...

    AgileJavaNote:在学习敏捷Java时记录我的笔记

    AgileJavaNote ###简介: 自学敏捷开发的思想,采用TDD 的方式开发一个学生信息系统 ###成果: 运用了UML 进行类的设计;对JAVA 中大部分的概念和特性动手编程实践;使用JUnit 对每个模块编写测试用例

    kharioki:我的个人资料

    嘿,我是Tony Stark :speech_balloon: 我是一名软件工程师,数字Nomad者...机器学习: Numpy,Pandas,Jupyter笔记本,Matplotlib,Scikit学习,Tensorflow。 TDD: Jest,Mocha,Chai,Cypress,React测试库 敏捷的

    模块化C代码与UML对象模型之间的映射(一)

    日子一天天过去,业余时间不多,为了避免生活华而不实,碌碌无为,我计划抽空把工作和学习中散落在笔记本和书本某些角落的总结整理出来,放到博客上备忘,同时也希望有机会和技术圈的朋友交流,增长自己的见识,我的...

    spa-tuts:学习如何使用 Tuts Plus 从头开始​​构建 JavaScript SPA

    使用生成的目录 删除视图骨干路由器单元测试和 TDD 什么才是好的单元测试? 测试设置网络服务设置模型和集合本地存储RequireJS 调试技巧本地存储和收藏模板下划线模板[小胡子](#...

    jungle-rails:用Rails 4.2构建的一个小型电子商务应用程序,用于通过示例学习Rails的目的

    由于这个项目,我了解了有关Ruby on Rails,模型-视图-控制器范例,嵌入式Ruby和测试(TDD和BDD)的更多信息。笔记: 已知问题:来宾(未经身份验证)用户仍然可以下订单。 下一步,应禁用此选项。主页管理员类别...

    shadow:一个可常用PHP公共资源包,同时也是百度文库课程《如何写好一个PHP的类》的

    课程的主要内容可以在“课程笔记”中找到,里面有每一节的主要内容讲解。 课程介绍 首先,这门课面向PHP开发者,主要讲解一个类库的建立,开发和使用。的是,这里面讲到的都是偏实用的东西。这里面会涉及到一些经常...

    rspec-webservice_matchers:黑匣子网络应用程序测试

    安装$ gem install rspec-webservice_matchers你得到什么这些新的RSpec匹配器: 笔记be_up 寻找200,但最多可追踪4次重新导向be_fast 检查Google 分数是否大于WEBSERVICE_MATCHER_INSIGHTS_KEY 85。 WEBSERVICE_...

Global site tag (gtag.js) - Google Analytics