上一章讲到“关于单元测试的常见错误观念和做法”,这一章我们通过实例讲讲“第一个单元测试”到底应该怎么做。

1. 需求

我们要测试一个银行账户类Account的“取款”工作单元——withdraw()方法。我们先定义这个方法的契约:

  1. 如果账户被冻结,取款将失败,并抛出AccountLockedException异常
  2. 如果取款金额是0或者负数,取款将失败,并抛出InvalidAmountException异常。
  3. 如果余额不足,取款将失败,并抛出BalanceInsufficientException异常。
  4. 如果上述情况都没发生,取款将成功,账户余额会相应扣减,并在系统中记录这一笔交易。

下面是关键的业务规则:

  1. 如果取款由于任何原因失败,账户余额不会发生任何变化。
  2. 如果取款成功,账户余额将会相应减少,并在系统中记录这笔交易。

2. 实现

2.1 被测类Account

基于上面的契约和规则,我们编写了下面的实现(此处暂不采用TDD,我们先写好产品代码,再编写测试):

package yang.yu.tdd.bank;//被测对象
public class Account {//内部状态:账户是否被冻结private boolean locked = false;//内部状态:当前余额private int balance = 0;//外部依赖(协作者):记录每一笔收支private Transactions transactions;//用于注入外部协作者的方法public void setTransactions(Transactions transactions) {this.transactions = transactions;}public boolean isLocked() {return locked;}public int getBalance() {return balance;}//存款工作单元public void deposit(int amount) {//失败路径1:账户被冻结时不允许存款if (locked) {throw new AccountLockedException();}//失败路径2:存款金额不是正数时不允许存款if (amount <= 0) {throw new InvalidAmountException();}//成功(快乐)路径balance += amount; //存款成功后改变内部状态transactions.add(this, TransactionType.DEBIT, amount); //存款成功后调用外部协作者}//取款工作单元public void withdraw(int amount) {//失败路径1:账户被冻结时不允许取款if (locked) {throw new AccountLockedException();}//失败路径2:取款金额不是正数时不允许取款if (amount <= 0) {throw new InvalidAmountException();}//失败路径3:取款金额超过余额时不允许取款if (amount > balance) {throw new BalanceInsufficientException();}//成功(快乐)路径balance -= amount;   //取款成功后改变内部状态transactions.add(this, TransactionType.CREDIT, amount); //取款成功后调用外部协作者}//冻结工作单元public void lock() {locked = true;}//解冻工作单元public void unlock() {locked = false;}
}

代码说明如下:

  • Account类有三个字段,其中locked和balance是两个内部状态,分别代表冻结状态和当前余额;transactions是外部依赖(协作者),用来记录存取交易。
  • Account类提供了isLocked()和getBalance()方法,分别将locked和balance内部状态暴露给外界。
  • Account类提供了lock()和unlock()方法来设置locked内部状态,deposit()和withdraw()来更改balance内部状态。
  • Account类提供了setTransactions()方法,用来注入外部依赖。

2.2 外部依赖Transactions接口

Transactions接口提供了记录每一笔存款、取款交易的方法add():

public interface Transactions {void add(Account account, TransactionType transactionType, int amount);
}

第一个参数记录交易关联的账户,第二个参数TransactionType是个枚举,表明是存款还是取款。第三个参数表示存取金额。

3. 单元测试

针对withdraw()契约和业务规则,我们编写下面一组单元测试来对它进行全面测试覆盖:

package yang.yu.tdd.bank;import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;public class AccountWithdrawTest {private static final int ORIGINAL_BALANCE = 10000;private Transactions transactions;private Account account;@BeforeEachvoid setUp() {account = new Account();transactions = mock(Transactions.class);account.setTransactions(transactions);account.deposit(ORIGINAL_BALANCE);}//账户状态正常,取款金额小于当前余额时取款成功@Testvoid shouldSuccess() {int amountOfWithdraw = 2000;account.withdraw(amountOfWithdraw);assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE - amountOfWithdraw);verify(transactions).add(account, TransactionType.CREDIT, amountOfWithdraw);}//将余额全部取完,也可以取款成功@Testvoid shouldSuccessWhenWithdrawAll() {account.withdraw(ORIGINAL_BALANCE);assertThat(account.getBalance()).isEqualTo(0);verify(transactions).add(account, TransactionType.CREDIT, ORIGINAL_BALANCE);}//账户被冻结,取款应当失败@Testvoid shouldFailWhenAccountLocked() {account.lock();assertThrows(AccountLockedException.class, () -> {account.withdraw(2000);});assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);verify(transactions, never()).add(account, TransactionType.CREDIT, 2000);}//取款金额是负数,取款应当失败@Testvoid shouldFailWhenAmountLessThanZero() {assertThrows(InvalidAmountException.class, () -> {account.withdraw(-1);});assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);verify(transactions, never()).add(account, TransactionType.CREDIT, -1);}//取款金额是0,应当失败@Testvoid shouldFailWhenAmountEqualToZero() {assertThrows(InvalidAmountException.class, () -> {account.withdraw(0);});assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);verify(transactions, never()).add(account, TransactionType.CREDIT, ORIGINAL_BALANCE);}//余额不足,应当失败@Testvoid shouldFailWhenBalanceInsufficient() {assertThrows(BalanceInsufficientException.class, () -> {account.withdraw(ORIGINAL_BALANCE + 1);});assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE);verify(transactions, never()).add(account, TransactionType.CREDIT, ORIGINAL_BALANCE + 1);}
}

上面的测试代码采用JUnit 5,Mockito 3和AssertJ 3编写。需要在JDK 8以上的版本运行。

说明:

  • 标注了@Test的方法是测试方法。方法没有返回值。一般情况下也没有参数。方法名字可以任意取,但最好能够充分表达测试意图。
  • 标注了@BeforeEach的方法,会在每一个测试方法执行之前都执行一次。方法名字可以任意取。

从上面每一个测试方法来看,每个测试通常都包含以下的过程:

  1. 创建被测对象。

java account = new Account();

  1. 设置内测对象的内部状态并注入外部依赖。对于单元测试,外部依赖应该用测试替身代替。

java //用Mockito创建测试替身,它实现了Transactions接口 transactions = mock(Transactions.class); //注入测试替身 account.setTransactions(transactions); //调用存款方法,设置初始余额 account.deposit(ORIGINAL_BALANCE); //调用冻结方法,设置冻结状态 account.lock();

  1. 调用被测试方法,执行测试。

java account.withdraw(amountOfWithdraw);

  1. 断言测试结果

成功时断言修改了内部状态并调用了外部依赖的方法:

java //断言当前余额等于原有余额减去取款金额 assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE - amountOfWithdraw); //断言调用了外部依赖transactions的add()方法,以account, TransactionType.CREDIT, amountOfWithdraw为参数 verify(transactions).add(account, TransactionType.CREDIT, amountOfWithdraw);

失败时断言抛出了期待的异常,余额没有减少并且没有调用外部依赖transactions来创建交易记录:

java //断言调用被测方法后抛出AccountLockedException异常 assertThrows(AccountLockedException.class, () -> { account.withdraw(2000); }); //断言余额没有减少 assertThat(account.getBalance()).isEqualTo(ORIGINAL_BALANCE); //断言没有调用外部依赖的方法 verify(transactions, never()).add(account, TransactionType.CREDIT, 2000);

上面的单元测试用到了本门课程将要介绍的三大框架:

  • JUnit用来编写测试的主体
  • Mockito用来创建外部依赖的测试替身,注入到被测对象。
  • AssertJ用来编写各种断言,断言单元测试的结果。虽然JUnit也包含了本身的断言库,但是内容不够丰富,形式不够优美。用AssertJ来写断言可读性等方面会好得多。

下一章将讲讲“测试哪些内容:Right-BICEP”!

- THE END -

原创作者 | 杨宇Yangyu

编程道与术原创内容

转载请注明“编程道与术”出处

mockito 外部接口_原创 |使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (五)第一个单元测试...相关推荐

  1. 原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (一)什么是单元测试

    If builders built buildings the way programmers wrote programs, then the first woodpecker that came ...

  2. 原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (六)测试哪些内容:Right-BICEP

    上一章通过实例讲了"第一个单元测试"到底应该怎么做,这一章我们讲讲"对一个工作单元需要测试它哪些方面的内容"? 有6个值得测试的部位,统称为:Right-BIC ...

  3. mockito 外部接口_【IDEA开发SpringBoot2.0】使用Mockito进行常规接口测试#05

    文章目录 前言[^1] 什么是Mockito? 什么是Mock? 为什么要使用Mock? 用PostMan与用Mock有什么区别? 正题 使用Mockito做一个模拟测试 编写代码 开始测试 代码讲解 ...

  4. 单元测试 代码里面都绝对路径怎么处理_原创 | 编写单元测试和实践TDD (六)测试哪些内容:Right-BICEP...

    上一章通过实例讲了"第一个单元测试"到底应该怎么做,这一章我们讲讲"对一个工作单元需要测试它哪些方面的内容"? 有6个值得测试的部位,统称为:Right-BIC ...

  5. junit mockito_使用JUnit 5在Mockito中方便地进行模拟–官方方式

    junit mockito 从版本2.17.0开始,如果使用了JUnit 5, Mockito提供了官方(内置)支持来管理模拟生命周期. 入门 为了利用集成的优势,需要在JUnit 5的junit-p ...

  6. 使用JUnit 5在Mockito中方便地进行模拟–官方方式

    从版本2.17.0开始,如果使用JUnit 5, Mockito提供了官方(内置)支持来管理模拟生命周期. 入门 为了利用该集成,需要在JUnit 5的junit-platform-engine旁边添 ...

  7. junit单元测试断言_简而言之,JUnit:单元测试断言

    junit单元测试断言 简而言之,本章涵盖了各种单元测试声明技术. 它详细说明了内置机制, Hamcrest匹配器和AssertJ断言的优缺点 . 正在进行的示例扩大了主题,并说明了如何创建和使用自定 ...

  8. mockito接口_什么是Mockito Extra接口?

    mockito接口 如果要编写轻量级的JUnit测试, Mockito是我最喜欢的小帮手. 如有必要,可以通过模拟轻松地替换被测单元的"实际"依赖关系,这非常有用. 特别是在处理框 ...

  9. junit mockito_JUnit和Mockito合作

    junit mockito 这次,我想对测试框架Mockito进行概述. 毫无疑问,这是用于测试Java代码的最受欢迎的工具之一. 我已经对Mockito的竞争对手EasyMock进行了概述. 这篇文 ...

  10. java junit mock_使用Mockito进行Java的Mock测试

    测试替身 dummy用于传递,不会真正使用,例如用于填充的方法的参数列表. Fake有简单实现,但通常被简化,比如在内存数据库,而不是真正的数据库中使用. Stub是接口或类中部分实现,测试时使用其实 ...

最新文章

  1. 提取so文件的特征值
  2. Java记录 -22- Java的基类Object详解
  3. 删除或卸载以前添加的库:cocoapods
  4. 结果集ResultDTO
  5. 【CyberSecurityLearning 40】网络地址配置(Kali/CentOS)
  6. Boost::context模块callcc的分段的测试程序
  7. Spring框架—IoC容器
  8. 如何在Windows上使用64位Web浏览器
  9. 没有docker,谈什么微服务架构?
  10. future.cancel不能关闭线程_彻底弄懂线程池-newFixedThreadPool实现线程池
  11. skyeye linux qt,ARM仿真器SkyEye的安装及使用
  12. java中什么是释放已经持有的锁_java多线程什么时候释放锁
  13. 联想投资服务器5g芯片,从5G投票到要没必要做芯片,联想到了最危险的时候
  14. 解读年度数据库PostgreSQL:如何巧妙地实现缓冲区管理器
  15. java模板和回调机制学习总结
  16. 传奇修改数据库后服务器异常,DBserver提示物品数据库加载错误的解决方法
  17. android 9.0 xposed,EdXposed管理器(安卓9.0专用)
  18. Could not load requested class
  19. 【Arduino】VC0706(中星微串口摄像头)
  20. java 蓝桥杯 基础练习(二)

热门文章

  1. 机器学习——关于感知机概念的理解(可能只对本人适用)
  2. 全网首发!超全SparseR-CNN实战教程
  3. 3D人脸重建算法汇总
  4. Windows Azure Cloud Service (42) 使用Azure In-Role Cache缓存(1)Co-located Role
  5. [置顶] 原创鼠标拖动实现DIV排序
  6. Java简单ztree树
  7. opensuse安装Tomcat碰到的问题
  8. mongodb基础操作
  9. wordpress模板
  10. (2)[wp7数据存储] WP7 IsolatedStorage系列篇——获取存储的文件或文件夹 [复制链接]...