当前位置: 代码迷 >> 综合 >> 单元测试、TDD、BDD
  详细解决方案

单元测试、TDD、BDD

热度:122   发布时间:2023-09-22 11:15:30.0

TDD(Test Driven Development)

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

TDD三定律(测试驱动开发)

1 、You are not allowed to write any production code unless it is to make a failing unit test pass.

除非为了使一个失败的unit test通过,否则不允许编写任何产品代码(必须先写测试用例)


2 、You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.

在一个单元测试中只允许编写刚好能够导致失败的内容(编译错误也算失败,测试用例必须有针对性)


3 、You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

只允许编写刚好能够使一个失败的unit test通过的产品代码(一个产品代码必须建立在一个测试用例成功的基础上,如果需要写新的产品代码必须从第1步开始)


测试代码


测试代码的要素:可读性。


编写测试用例的模式,或者现有的测试框架提供的测试形式是,构造-操作-检验(BUILD-OPERATE-CHECK)模式。

其分为三个环节:

1、构建测试数据;

2、操作或处理测试数据(即数据进入真实代码走一遭);

3、校验真实代码的输出结果是否与预期的一致。

一些测试建议

1、整洁的测试

2、尽量每个测试一个断言,不绝对。但是越少越好(针对性越强)

3、每个测试一个概念,不要混合多个逻辑,不要写很长的测试函数。


测试的5条(FIRST)准则:

1 快速(Fast),测试应该快(及时反馈出业务代码的问题)。

2 独立(Independent) 每个测试流程应该独立。

3 可重复(Repeatable) 测试应该在任何环境上都能重复通过。

4 自我验证(Self-Validating) 测试应该有bool输出。

5 及时(Timely) 测试应该及时编写。


Unit test at Google

1.单元测试对代码维护的重要性

如果为了在原来代码上加简单的新功能,导致了很多报错。可能会导致新的开发要重新调试所有流程,还要重新测试所有流程进行验证。那是非常痛苦切影响效率的。而且有两个问题点是无法避免的:1.他的测试是脆弱的(brittle test),他可能引入一些有问题但是又和他不相干的问题 2.测试是不清晰的,很难发现那个位置是错的,如何修复,以及这些测试首先应该做什么。

2.避免写脆弱单元测试

3.力求不改动测试用例

原则:理想情况下,测试用例应该是不变的,除非系统需求变更。

坚持本原则会编码带来四个基本改变:

(1)纯粹的重构(pure refactorings)

不改变系统接口的情况下,无论是优化性能还是提高代码清晰度的这类重构,都是不需要修改单元测试的。在这种情况下,测试的作用是确保重构没有改变系统的行为。测试过程中需要更改的测试重构表明要么是更改影响了系统的行为,要么是这不是一个纯粹的重构(第二十二章有谷歌进行一次大规模重构的案例)

(2)新功能(new features)

当工程师在原来系统上新增功能时,应该让原本存在的功能、行为(behaviors)不受影响。和重构一样,不应该对原有测试进行修改,以免造成不可预估后果。

(3)bug修改(bug fixes)

修改bug就想新增一个功能。bug的存在就表明确实了一个测试用例或者多个测试用例,所以修复bug也包含补充相应测试用例

(4)系统行为变化(behaviors change)

这种系统原有功能、行为的变更是需要修改测试用例的情况。系统的用户可能会依赖于其当前行为,而对该行为的更改需要协调与这些用户联系以避免混淆或中断。这种行为的代价是比前面三种都要大的。在这种情况下更改测试表明我们正在破坏系统的原有的约定(在之前的案例中,我们需要遵守的约定)

4.用公共的API(Pubilc API)进行测试

使用你接口或者处理类的方法直接进行测试,保证用户使用的代码方法和你测试的一致。这样的测试更真实,也不那么脆弱因为它们形成了明确的契约:如果这样的测试失败,那么意味着系统用户行为也会被影响和破坏。Public API, 可能是一个比较模糊的概念,有可能来自一个工具类、一个helper类、甚至第三方库的方法,可能需要根据实际情况编写单独的单元测试,或者单元测试单独覆盖这个方法(虽然会有冗余,但是长远来看可能是利大于弊的)

5.测试状态,而非交互(Test State, Not Interactions)

个人理解是,测试用例应该专注于本身期望返回的状态,而不该与其他系统或者协作者交互协作的期望结果上。

// brittle interaction test
@Test
public void shouldWriteToDatabase() {
accounts.createUser("foobar");
verify(database).put("foobar");
}

说明:1.调用数据库API进行交互,结果可能受到干扰; 这里未理解,原文:

The test verifies that a specific call was made against a database API, but there are a couple different ways it could go wrong: ? ? If a bug in the system under test causes the record to be deleted from the database shortly after it was written, the test will pass even though we would have wanted it to fail.(数据库记录在写入后马上被删除,但是测试还是能通过(期望失败)。 ? ? If the system under test is refactored to call a slightly different API to write an equivalent record, the test will fail even though we would have wanted it to pass.(重构后换了新的api写相同的数据,但是测试却失败了(期望通过)。

//Testing against state
@Test
public void shouldCreateUsers() {
accounts.createUser("foobar");
assertThat(accounts.getUser("foobar")).isNotNull();
}

6.写清晰的单元测试(Writing Clear Tests)

尽管我们尽可能地避免了脆弱的单元测试,但有时我们还是会测试失败。这也是好事,因为提供这个错误的信号是单元测试的一大价值.

测试失败的原因,书中总结了两种主要情况:

1.系统在测试后有问题或者不完善,这也是我们设计测试用例的原因:对我们的bug进行警示,方便我们去修复他

  2.这个测试本身就是糟糕的测试。系统在测试后没有问题,但是这些测试确实是不正确的。如果他是一个已经存在的测试,而不是你刚写的,那这应该是一个脆弱的测试。只能尽可能避免些这类脆弱的测试。

测试如果失败,发现问题的速度是取决于我们单元测试的清晰度(Clarity)的。保证一个测试的范围和长期可用,对每个单元测试一定要尽可能的清晰。

7.让你的单元测试完整和简洁(completeness and conciseness)

书中总结了两个帮助测试变得清晰的两个主要性质:完整性和简洁性

不整洁的测试代码里会包含一些无关联的信息,阅读起来需要去理解正确结果是如何获得。

   ```java// 反例@Testpublic void shouldPerformAddition() {Calculator calculator = new Calculator(new RoundingStrategy(),"unused", ENABLE_COSINE_FEATURE, 0.01, calculusEngine, false);//过复杂且和测试无关的代码int result = calculator.calculate(newTestCalculation());//信息被隐藏在newTestCalculationassertThat(result).isEqualTo(5); // Where did this number come from?}```
//正例
@Test
public void shouldPerformAddition() {Calculator calculator = newCalculator();int result = calculator.calculate(newCalculation(2, Operation.PLUS, 3));assertThat(result).isEqualTo(5);
}

8.面向行为测试,而非方法(Test Behaviors, Not Methods)

TDD的不足:

  • 它解决的是代码级的验证,但是测试代码与需求的符合问题解决得不是很好,非技术人员、客户看不懂代码,无法评审测试是否符合需求。

  • 测试代码可能写得太大或者太小,令开发人员效率下降。这与测试代码与功能对应不起来有很大关系。

BDD(Behaviour-Driven Development): https://dannorth.net/introducing-bdd/

BDD的由外而内的开发模式(Outside-In Development)

开发人员使用BDD工具(JBehave, Cucumber, Behave)去运行、实现测试脚本。再一点点编写实现功能代码,走到所有的功能都运行通过。

由于开发的过程是从最接近用户的UI界面开始,再想到内部设计,因此它称为由外而内的开发模式。

它需要遵循一定的简单语法

  • Scenario(场景),说明功能的例子

  • Given(假如),构造测试的环境条件

  • When(当),给予的输入,可以是用户,也可以是外部系统,也可以是系统本身定时/条件触发的

  • Then(那么),系统的输出,或者说行为

    若干个Given,When,Then构成一个Scenario,若干个Scenario构成一个Feature,若干个Feature最终构成一个系统的完整功能需求

  相关解决方案