`
chelsea
  • 浏览: 117884 次
  • 来自: ...
社区版块
存档分类
最新评论

TDD: Tricky Driven Development

    博客分类:
 
阅读更多

 

命名

测试用例的名字应该描述需求, 不要描述实现.

取决于你要沟通交流传递的信息, Test Case 有至少两个作用

 

  1.  

    检查你的产品代码是否按预期工作, 这由函数体来完成

     

  2.  

    表达你的预期,让阅读代码的人知道你的产品能够干什么,如何使用, 甚至如何设计的;这除了函数体的assert语句外,Test case的名字更是重要的手段

     

但我们通常只会为一段测试代码起一个名字, 而要表达的信息如此之多, 怎么办? 一个测试用例尽量只有一个断言, 或至少限制在一类断言, 这时候你就能够起一个Domain相关的名字

如果Domain比较复杂, 进入项目的新人不一定了解, 这时候你的测试用例的名字就是最好的领域知识, 比如电信计费系统:

 

public void testShouldBeFreeFrom2amTo5am() throws Exception {

 

 

Duration talkingDuration = new Duration("2am", "5am");

 

 

Money fee = chargeSystem.charge(talkingDuration);

 

 

assertEquals(Money.ZERO, fee);

 

 

}

 

Setup

所有测试相关的代码, 包括设置测试环境, 调用被测对象触发测试, 断言测试结果等, 应该放在一起,便于阅读和理解; 那setUp()里放什么?

  1. 如果一个对象很容易用一两句话装配, 一般可以在每个测试用例里就近创建它

  2. 如果初始化一个对象很复杂, 要不少代码, 就写一个函数来做初始化, 然后在每个测试用例里就近调用它

  3. setUp()里放点公用的基础的比如对外部依赖环境的设置

 

Mock stub

1, 真实数据/环境

随着自动化单元测试, Mock技术的流行, 人们似乎逐渐的鄙视使用真实的环境和数据来测试; 但真实的环境和数据并不是天然就与自动化无缘, 只要它可以很容易的获得,并能够伴随测试代码一起发布

这方面典型例子是文件系统, 可以使用相对路径消除对存放位置的依赖, 使用统一的入口如 TestFilesUtil 来负责对测试数据的访问.

真实数据/环境的好处是它最接近生产环境, 可以通过提供不同的数据来产生不同的测试用例, 比较方便的提高测试覆盖率

真实数据/环境的坏处就是应用范围比较窄,虽然有"嵌入式"的数据库和FTP服务器等,但很少在单元测试中使用;我们主要是在测试以文件作为输入的API的时候使用真实的文件系统

2, 静态手写Stub

这是初期比较常用的方法, 是State Based Test方法的实现方式 (与之对应的Interaction Based Test/ Behaviour verification Test的实现方式是Mock Object, 或者Classical TDD vs. Mocklist TDD, 反正就是这么两个意思

好处是简单, 易于获得, 符合既有的思维习惯, 坏处是繁琐, 最终测试代码中充斥着大量Stub类

Stub类的编写应该遵循以下原则:

  • 不要包含任何逻辑;

就是所有函数都简单的return一个固定的结果; 一旦你的Stub包含逻辑, 你就需要为你的Stub也写一个测试了, 呵呵, 我们的系统中现在就有这么一个Stub, 陪伴它的是它的测试用例

Stub 会带来一个好处:

  • 强迫你重新考虑你的设计 (其实这几乎是任何高质量的测试用例都能带来的)

但Stub尤其会在两个方面强迫你重新思考:

1), 调用链

就是当你的产品代码中出现如下调用时 DateTime buildDate = obj.getConfigure().getProject().getBuild().getDate();

你如何使用Stub来测试这段代码呢? 很不幸, 你需要一鼓作气写三个Stub类分别代理Configure/Project/Build才能完成测试; 这就强迫你重新思考, 原来的设计是否有问题, 是否需要重构

2), 针对接口编程

OO完美主义者倾向于用构造函数建立起一个不变式, 所以经常在构造函数中进行各种计算和验证, 一旦发现不符合不变式就会抛出异常之类; 这就给Stub带来一个问题, 因为要调用基类的构造函数, 传递什么样的参数才能不让基类的构造函数抛出异常呢? 通常在测试环境中我们不容易满足这类约束, 这就迫使我们抽取本来就应该抽取的接口, 将函数的参数类型或返回值类型替换为该接口, 然后只要Stub实现这个接口即可

 

3, Mock Object

真实数据是测试世界的北极, Mock Object就是南极

Mock Object的经验不多, 所以首先感受到的是它的不便之处: 重构会破坏测试用例, 即使你的重构是正确的

与有的Mock Object实现对重构的"Rename"之类的操作支持不好相比, 重构直接破坏测试用例更郁闷:

  1. 比如你用一个现有的充分测试过的第三方的API替换了你自己写的几行实现

  2. 比如你删除了几行冗余的调用

  3. 甚至你把几个public函数中重复逻辑抽取到一个私有函数里, 都有可能破坏基于Mock Object的test case.

然而与Mock Object的误用相比, 重构破坏测试用例便不算什么了. 毫无疑问有些情况下是不应该使用Mock Object的, 这尤其体现在那些紧凑的API上, 即单一API调用, 根据不同参数返回不同结果; 这类API包括:

  1. 传入一个xpath表达式, 返回NodeList

  2. 传入正则表达式, 返回匹配结果

  3. 传入SQL语句, 返回结果集

这类API的返回结果强烈依赖于它们的参数, 参数才是它们的核心, 你一旦在你的产品代码中使用了这些API, 又在测试用例中Mock了这些API, 直接返回固定的结果, 那么恭喜你, 你的测试白做了. (这类API对Stub也不感冒, 尽可能用真实数据来测)

Mock Object的适用场景其实和Stub差不多, 首要目的是减少对系统其它部分包括外部系统的依赖. 但Mock对交互顺序和参数/返回值传递强大的支持可以使你更精确的断言你的代码的行为 考虑经典的用户存款场景, 假设在此过程中, 你的API会进一步调用银行API来完成操作, 如果使用State Based 测试方法, 我们的测试用例可能只是在调用你的API前后断言一下用户账户的balance就算了, 比如:

public void testBalanceShouldIncrease50WhenDeposit50() throws Exception {

 

 

double balanceBeforeDeposit = getBalance();

 

 

deposit(50);

 

 

double balanceAfterDeposit = getBalance();

 

 

assertEquals(balanceBeforeDeposit + 50, balanceAfterDeposit);

 

 

}

 

 

通常这样的测试也算测过了, 但这样我们无法测试钱是直接进了你的账户., 还是中间流转了一下; 如果金融系统的客户对自己内部的系统要求比较严格, 你可能需要对这中间的内部调用逻辑进行测试, 以避免洗钱之类的可能, 这时候你就可以用Mock来断言银行的API确实以参数50被调用了一次, 而不是以参数25调用了两次

public void testDepositShouldPutMoneyToYourAccountDirectly() throws Exception {

 

 

depositMock.expect(once()).method("deposit").with(eq(50));

 

 

 

 

double balanceBeforeDeposit = getBalance();

 

 

deposit(50);

 

 

double balanceAfterDeposit = getBalance();

 

 

assertEquals(balanceBeforeDeposit + 50, balanceAfterDeposit);

 

 

}

 

 

Mock Object 还支持交互顺序的测试. 比如你有两个操作, 必须以确定的顺序来执行, 你担心后续的维护者会破坏这种约定, 则可以使用 Mock 测试显式的描述它.

关于 Mock 和 Stub 的其它描述, 请参考 <<敏捷质疑: TDD>>

 

测试杀手

 

1.

static

method,

new

operator

 

 

这是一个Spring的时代了, 你还在用

static

method 吗? 还在用

new

operator 去创建对象吗? 仅仅"让你的API难以测试"这一条便足以宣判他们的死刑了

 

 

记得很早以前就写过: "

RAII让我告别了delete,IoC让我告别了

 

new

 

"

 

关于 static

 

new 的测试, 请参考<<假冒的艺术>>

 

 

 

 

 

2. Prolonged failed test case

 

 

"小步前进"是确保TDD成功的众多因素中的一条, 更早以前还写过: "

目标驱动生活,每天早上运行一遍测试用例:assertTrue(有房);assertTrue(有车),测试失败,就努力让它早日通过

"

 

 

感谢当年提出"步子太大,这个测试fail的时间太长"意见的朋友,Blog的变迁使得这条评论已经不见了, 但真理亘古不变,我现在决定把它改成"assertTrue(每月咱也打回的); assertTrue(不要顿顿三明治)"

 

 

 

测试朋友

 

1, 谁是谁的测试,

当你重构测试用例的时候?

 

当你重构产品代码的时候, 测试用例是你的朋友, 可以确保重构不会引入错误;

那么当你的测试越来越多, 需要重构一下便于增加新的测试的时候, 谁是谁的测试呢?

 

2. IDE

我的设想是强迫你TDD的IDE; 现在的IDE, 如Eclipse, 它的新建Class的向导不会让你选择这个新类需要满足的测试用例, 而它的新建测试用例向导会让你选择打算测试哪个类

新的支持TDD的IDE会反过来, 你不能凭空新建一个类, 除非它是测试用例类, 当你新建类的时候, 你必须指定它被用来满足哪个失败的Test Case

 

3.评审员

直接从以前的Blog中搬过来: "测试一词是个错误的用法,表达了一种实现,手段,过程,而不是目标、结果;一个副作用就是令测试人员不被重视;代码复审和系统测试目的都是找出编码中的错误,但一种叫评审员,一种叫测试人员;建议在目前出现测试一词的地方,都替换为“验证”,如系统验证,验证人员,“xxx,那个bug我改了,你再帮我验证一下”,呵呵,颠覆一把话语体系"

 

分享到:
评论

相关推荐

    Test-Driven Development with Python [2017]

    By taking you through the development of a real web application from beginning to end, the second edition of this hands-on guide demonstrates the practical advantages of test-driven development (TDD) ...

    TDD(Test-Driven Development)的Demo

    TDD(Test-Driven Development) 测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD得原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法...

    Test Driven Development: By Example

    ### Test-Driven Development (TDD): By Example #### 引言 在软件开发领域,测试驱动开发(Test-Driven Development, TDD)是一种被广泛推崇的方法论,它提倡在编写实际代码之前先编写测试用例。这种方法不仅能...

    Test-Driven Development: A Practical Guide

    **测试驱动开发(Test-Driven Development,TDD):实践指南** 测试驱动开发是一种软件开发方法,由Kent Beck在2003年提出并推广,它强调先编写测试,然后编写满足这些测试的代码。TDD的核心原则是“测试先行”,即...

    Test-Driven Development(TDD).pptx

    Test-Driven Development(TDD)简介,使用Nunit开发自动测试案例.

    Test-Driven Development with Python 【第二版】

    the updated second edition of this hands-on guide demonstrates the practical advantages of test-driven development (TDD) with Python. You’ll learn how to write and run tests before building each ...

    Test-Driven Java Development(PACKT,2015)

    Test-driven development (TDD) is a development approach that relies on a test-first procedure that emphasises writing a test before writing the necessary code, and then refactoring the code to ...

    Practical Test-Driven Development using C# 7.epub

    Test-Driven Development (TDD) is a methodology that helps you to write as little as code as possible to satisfy software requirements, and ensures that what you've written does what it's supposed to ...

    TDD:通过大量测试寻找最优解决方案.docx

    标题中的"TDD"指的是Test-Driven Development,即测试驱动开发,是一种软件开发方法论,强调在编写实际代码之前先编写测试用例。这种方法的核心理念是通过编写测试来定义需求,确保代码的质量和功能完整性。TDD的...

    测试驱动的软件开发 TDD (Test-Driven Development)+敏捷开发过程

    测试驱动的软件开发(TDD,Test-Driven Development)是一种编程实践,强调在编写实际代码之前先编写测试用例。这种开发方式的核心理念是“先写测试,再写代码”。TDD 的工作流程通常分为三个步骤:红、绿、重构。 ...

    Behavior-Driven Development 详解

    相较于传统的 Test-Driven Development(TDD),BDD 更加关注于描述软件的期望行为而非具体的实现细节,从而帮助开发者更好地理解用户需求,确保所开发的软件既能适应不断变化的需求,也能保持高度的相关性。...

    Agile Java Crafting Code with Test-Driven Development

    Presents an expert overview of TDD and agile programming techniques from the Java developer's perspective Brings together practical best practices for Java, TDD, and OO design Walks through setting ...

    Type-Driven Development with Idris

    类型驱动开发(Type-Driven Development,简称TDD)是一种软件开发方法论,它强调通过类型系统来指导程序的设计和开发过程。在Idris中,类型驱动开发的关键特性包括: - **依赖类型**:Idris支持依赖类型,这意味着...

    Test-Driven Java Development

    Test-driven development (TDD) is a development approach that relies on a test-first procedure that emphasises writing a test before writing the necessary code, and then refactoring the code to ...

    Test Driven Development, A J2EE example

    Test Driven Development: A J2EE Example by Russell Gold, Thomas Hammell and Tom Snyder ISBN:1590593278 Apress © 2005 (296 pages) Targeted at Java developers who want to learn how to use test-...

    Test-driven development

    **测试驱动开发**(Test-Driven Development,简称 TDD)是一种软件开发方法论,强调在编写任何功能代码之前先编写测试用例。这种方法能够确保软件的功能符合预期,并且有助于提高代码的质量和可维护性。 #### 二、...

    Android Test Driven Development Tutorials - v1.zip

    在Android开发领域,Test Driven Development(TDD)是一种被广泛应用的编程实践,它强调先编写测试用例,再编写满足这些测试的代码。在这个"Android Test Driven Development Tutorials"资源包中,包含了PDF、ePub...

Global site tag (gtag.js) - Google Analytics