大厂技术专家手把手教你如何写好单元测试
前言
说起单元测试,很多小公司不注重单元测试,从而导致了上线后出现各类奇怪问题,很大一部分开发人员都没有编写单元测试的习惯,但去了大厂之后才知道差距在哪里!单测是系统稳定的保障,不容不引起重视,写好单测是优秀的程序员的必备技能。本期给大家带来以下几个部分:
- 单元测试基本概念讲解
- 常用单元测试框架讲解
- 单元测试规范及需要注意的内容
- 实战代码讲解各类情况单元测试
- 使用 Mockito 进行复杂业务单测
- 大厂单元测试实战经验总结分享
注:本期的实战演练以 Spring Boot 为基础,版本为 2.5.1(官网最新稳定版本)。
单元测试的基本概念
我们先看一下百度百科是如何对单元测试进行概括的:
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。—— 百度百科
简单来说,单元测试就是对一个最小的功能单元进行测试,单元测试和程序是相互隔离的。那么在 Java 当中,最小的功能单元无非就是方法(函数)了。一个类中有很多的方法,而一个类往往对应一个单元测试类,类中每个方法,都在测试类中需要对应一个单元测试。也就是一个标注了 @Test 注解的方法。
在单元测试中我们可以设定程序预期执行返回的结果,如果一些正常,则单元测试会表现为 pass,而如果单元测试的执行结果和我们预期的结果不一致,则会表现为 fail。提示我们,代码逻辑存在问题,需要进行改正。
下面说一下单元测试的文件结构,现在 Java 项目几乎都是使用 Maven 来进行构建,其实我们参照 Maven 的文件标准就可以了。下面是 Maven 的单元测试结构:
源代码一般存放在 src/main/java 目录下,单元测试一般存放在 src/test/java 目录下。
注意: 单元测试类的包层级和被测类的包层级是一样的,且一般以被测类名后加 Test 或 Tests。
单元测试初体验
1. 先创建一个 Spring Boot 项目,版本为官网最新稳定版 2.5.1,如上图所示,然后加入以下依赖(一般使用 Spring Boot 初始化向导是默认自带的,没有的可以添加下面的依赖)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
参考官方说明:
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing
我们再加入测试框架 Mockito、PowerMockito 的相关依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
2. 创建以下文件,如图所示:
3. 在 JunitService.java 中写入以下代码:
/**
* Junit Service
*/
@Service
public class JunitService {
/**
* 返回用户传入的内容
*
* @param content 内容
* @return 用户传入的内容
*/
public static String retString(String content) {
return content;
}
}
4. 在 JunitServiceTest.java 中写入一下代码,并添加 @Test 注解。
public class JunitServiceTest {
@Test
public void testRetString() {
String content = "content";
String res = JunitService.toResString(content);
// 使用断言结果
Assert.assertEquals(content,res);
}
}
5. 右键执行单元测试,我们可以看到控制台打印如下:显示 Tests passed。
6. 至此我们已经完整的体验了一个单元测试的编写过程。
常用单元测试框架
我们在 Spring Boot 官方文档里,单元测试章节看到以下内容:
大部分企业中,JUnit 一般是基础框架(必须),然后搭配使用 Mockito 和 PowerMockito。我们本节重点说一说 Mockito。
常用测试框架详解
什么是 Mockito?
说起 Mockito,大多数人下意识觉得很熟悉,因为这是一款酒的名字,中文名叫莫吉托。Mockito 是一个非常好用的 Mock 框架,它让可以让你使用干净简洁的 api 写出优雅的单元测试。Mockito 不会让你感到宿醉,因为单元测试的可读性很强,而且如果测试结果和预期结果不匹配,它们会产生简洁的验证错误。
官网地址:https://site.mockito.org/
GitHub 地址:https://github.com/mockito/mockito
Mockito 快速精通
引入 Maven 依赖
参考上一节的依赖。
验证交互行为
// 使用 mock 创建 List 对象
List mockedList = mock(List.class);
// 使用 mock 对象操作方法
mockedList.add("one");
mockedList.clear();
// 验证操作
verify(mockedList).add("one");
verify(mockedList).clear();
假设返回结果
这是我认为 Mockito 最厉害的功能了!我们看下面一段代码:
// mock 对象, 可以是类或者接口
LinkedList mockedList = mock(LinkedList.class);
// 当 list 执行 get(0)操作时, 直接模拟它返回 first
when(mockedList.get(0)).thenReturn("first");
// 当 list 执行 get(1)操作时, 直接模拟它抛出 RuntimeException 异常
when(mockedList.get(1)).thenThrow(new RuntimeException());
//控制台打印结果 "first"
System.out.println(mockedList.get(0));
//控制台打印结果抛出 runtime exception
System.out.println(mockedList.get(1));
//控制台打印 “null”,因为我们没有假设 mockedList.get(999) 的返回结果
System.out.println(mockedList.get(999));
// 验证方法
verify(mockedList).get(0);
这里需要特殊说明一下,默认情况下,对于有返回值的所有方法,模拟将会返回 null,基本数据类型或者基本数据包装类型或者空集合会视情况而定。例如返回值类型为 int 的,则默认返回 0。
如果我们模拟方法执行返回结果,其原理就是使用我们模拟的结果,将原有的结果进行覆盖。
参数匹配
//mockedList.get()方法,我们不传入实际的参数,采用 anyInt()。表示传入
// 任意一个整形都返回 element
when(mockedList.get(anyInt())).thenReturn("element");
如果你想使用参数类型匹配,那么所有的传入的参数都必须匹配。
verify(mock).someMethod(anyInt(), anyString(), eq("3 argument"));
// 验证 someMethod 方法, 参数:int,String ,和指定的字符 "third argument"
verify(mock).someMethod(anyInt(), anyString(), "3 argument");
// 如果我们不使用 eq 方法指定传入的字符,而是直接传入实参,则会抛出异常
验证确切的调用次数,至少 x 次 / 一次都没有
//使用 mock 创建的 mockedList, 添加一些执行次数
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//验证方法,以及方法执行次数 - times(1) 表示默认执行一次
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
//验证指定调用次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
//使用 never()方法验证. never()方法等同于 times(0)
verify(mockedList, never()).add("never happened");
//使用 atLeast():至少 多少次 /atMost() 最多 多少次
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");
模拟方法抛出异常
日常开发过程中,我们经常需要对一些异常情况进行从处理,使用模拟抛出异常让测试变得更加轻松。
// 模拟:当 mockedList 调用 clear()方法时,抛出 RuntimeException 异常
doThrow(new RuntimeException()).when(mockedList).clear();
//执行方法, 抛出 RuntimeException:
mockedList.clear();
验证顺序
注:必须以特定顺序调用其方法的单个模拟。
// mock 对象
List singleMock = mock(List.class);
//使用 mock 对象
singleMock.add("was added first");
singleMock.add("was added second");
//创建一个 inOrder 用于验证 singleMock
InOrder inOrder = inOrder(singleMock);
// 按方法执行的顺序一个一个验证, 如果未按照顺序则会抛出异常
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
这个场景也是比较好用的,例如我们的业务有强制的先后执行顺序,使用这个就比较好验证。
验证从未发生交互
//mock 一个 mockOne, 执行 add("one")操作
mockOne.add("one");
//验证 mockOne 从未执行过 add("two")操作
verify(mockOne, never()).add("two");
标准 Mock 创建对象
注:使用 @Mock 注入相关依赖,使用 @InjectMock 注入被测试类。顺序时 @InectMock 在上,@Mock 在下。还必须在单元测试类上标注 @RunWith(MockitoJUnitRunner.class) 注解启动,JUint4 和 JUnit5 有些差别,具体可查阅官方文档。
// 使用 @Mock 注解注入对象
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
doReturn()、doThrow()、doAnswer()、doNothing()、doCallRealMethod() 方法族
- doReturn():模拟结果返回
- doThrow():模拟抛出异常
- doAnswer():模拟执行 void 方法时,返回结果
- doNothing():模拟什么都不做,一般用的很少
- doCallRealMethod():模拟调用真实的方法
监控真实对象
你可以创建一个真实对象,然后使用 spy() 方法时,将调用真正的方法。除非我们模拟了结果返回。
List list = new LinkedList();
List spy = spy(list);
//模拟返回结果
when(spy.size()).thenReturn(100);
//调用真实方法
spy.add("one");
spy.add("two");
//第一个属性打印 "one"
System.out.println(spy.get(0));
//打印 size() ,结果 “100”
System.out.println(spy.size());
//也可以进行验证
verify(spy).add("one");
verify(spy).add("two");
验证超时
// 执行超过 100ms 超时
verify(mock, timeout(100)).someMethod();
// 执行一次超过 100ms 超时
verify(mock, timeout(100).times(1)).someMethod();
单元测试规范及注意事项
俗话说的好,没有规矩,不成方圆。一个程序员专不专业,一看编码风格,代码规范就能看出来。单元测试一样如此,那么我们就来说一说单元测试中的规范和注意事项。
一一匹配
这里说的是有实际意义的一个类,必须有一个对应的单元测试类。大家可以简单理解为男女搭配,干活不累。
- 包层级匹配,即类路径下的包层级是一样的。例如:业务类是 com.jason.service.MyService.java 那么测试类就是 com.jason.service.MyServiceTest.java,这样做的好处是快速能定位测试类的位置,省去查找的时间。
- 类名匹配:例如:业务类类名 MyService,对应的测试类类名 MyServiceTest,即测试类后加 Test,也是方便查找和定位
那些类需要单测
是不是所有的类都需要单测呢?其实不是的,当然,绝大部分都是需要的,只有一些少部分无实际测试意义的类,就不需要单测。除了以下这些,其他的都需要单测,即使是 model、dto 等对象模型也需要。这样我们即使改动了某些字段,单测也能帮我们检测出来,提醒我们其他地方有使用到该模型,需要进行覆盖。
说一个常见的场景: 小明因业务需求,给 dto 增加了一个字段,没有写单测。在很多地方都用到了 dto,但是有一个方法小明忘记对 dto 增加的字段进行 set 操作,导致某些场景下,该字段的值为 null,从而导致了 bug 的产生。如果我们有单测,且覆盖到,就可以完全杜绝这个问题。单测就是白盒测试。
那些类不需要单测:
- 接口不需要单测,接口的实现类需要单测
- 抽象类不需要单测,抽象类的实现类需要单测
- 常量不需要单测
分场景覆盖
if(sex == 0) {
...
}else if(sex == 1) {
...
}else{
...
}
阅读以上代码,我们可以发现,正常一次方法调用只能走一个分支,但是我们的单测需要覆盖所有分支,这样才能确保每种情况下都能执行我们正确的预期。
// 按照不同分支进行测试
@Test
void testExcuteWhenSexIsZero() {
...
}
@Test
void testExcuteWhenSexIsOne() {
...
}
@Test
void testExcuteWhenSexIsOther() {
...
}
异常测试
开发过程中,是不是遇到异常就直接抛出去或者 catch 住?这种情况下,如果产生了异常,代码的执行顺序或许和实际的执行顺序不一样。所以对于方法抛出的异常,也是非常有必要进行覆盖的。
@Test
void testThrowException() {
when(someMethod()).thenThrows(new NullPointException());
}
运行单测
写完单测一定要记得执行,查看运行结果是不是 pass 通过的,如果不是则需要及时修改单测。很多时候写完忘记运行单测,导致可以预期的错误却没有发现,从而产生了线上问题。整体单测,可以使用 Maven 的 mvn test 组件完成。
必须要的断言
我们要求,每一个单测都要有断言,没有断言的单测将毫无意义。断言可以判断执行的结果是不是我们预期的结果,可以保证程序执行的正确性,保障程序的健壮性。
@Test
void testGetUserInfo() {
Student student = mock(Student.class);
student.setName("zs");
// 断言:student 不为 null
Assert.assertNotNull(student);
// 断言:student 的 name 为 zs
Assert.assertEquals("zs", student.getName());
}
单测名称需见名知意
单测的名称极为重要,很多时候你看到一个单测名称都不知道作者想要测得是什么内容,如果加上没有写注释,那更加是云里雾里。有意义的单测名,可以极大地提升代码的可读性,提升效率。
// 错误示范:测试支付订单
@Test
void testPayOrder() {
}
// 正确示范:具体化
// 测试:支付订单, 当用户使用支付宝支付时
@Test
void testPayOrderWhenUseAlipay() {
}
实战代码讲解各类情况单元测试
本节讲解采用代码 + 注释的方式进行讲解,请大家阅读源码的同时结合注释进行理解。
测试普通静态方法
@Service
public class UserServiceImpl implements UserService {
public static boolean checkStringIsNotNull(String str) {
return str == null;
}
}
public class UserServiceImplTest {
/*
* 测试: 检查字符不等于 null
*/
@Test
void testCheckStringIsNotNull() {
String str = null;
// 断言:判断成功
Assert.assertTrue(UserServiceImpl.checkStringIsNotNull(str));
}
}
测试对象方法
@Service
public class UserServiceImpl implements UserService {
public boolean checkStringIsNotNull(String str) {
return str == null;
}
}
// 想要使用注解注入对象, 必须要使用@RunWith 启动
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class UserServiceImplTest {
// @Mock 注入对象, 注意:@Mock 时根据 id 注入的
@Mock
UserServiceImpl userServiceImpl;
// 执行测试之前执行
@Before
void setup() {
// 初始化 mock 注入环境
MockitoAnnotations.initMocks(this);
}
/*
* 测试: 检查字符不等于 null
*/
@Test
void testCheckStringIsNotNull() {
String str = null;
// 断言:userServiceImpl 对象不为 null
Assert.assertNotNull(userServiceImpl);
// 断言:判断成功
Assert.assertTrue(userServiceImpl.checkStringIsNotNull(str));
}
}
测试方法中有其他依赖
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public User getUserDetail(Long id) {
return userDao.selectUserById(id);
}
}
此时 userService 对象依赖了 userDao 对象,我们需要测试的方法包含了其他依赖的情况。
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class UserServiceImplTest {
// @InjectMock 注入被测对象, 一般写在最前面
@InjectMock
UserServiceImpl userServiceImpl;
// @Mock 注入需要的依赖对象
@Mock
UserDao userDao;
// 执行测试之前执行
@Before
void setup() {
// 初始化 mock 注入环境
MockitoAnnotations.initMocks(this);
}
/*
* 测试: 获取用户详情
*/
@Test
void testGetUserDetail() {
// mock 一个 User 对象
User user = mock(User.class);
//断言
Assert.assertNotNull(user);
// 设置属性
user.setName("张三");
// 模拟 UserDao 查询返回结果,使用 anyLong()代表任意 Long 类型的参数. 模拟返回我们 mock 的 user 对象
when(userDao.selectUserById(anyLong())).thenReturn(user);
// 执行需要测试的方法
User userInfo = userServiceImpl.getUserDetail(anyLong());
// 断言:userInfo 不为 null
Assert.assertNotNull(userInfo);
// 断言:user.getName() 等于 userInfo.getName()
Assert.assertEquals(user.getName(), userInfo.getName())
}
}
测试方法调用工具类
很多时候,我们都会用到静态工具类,这些是不经过对象调用的。
public final class StrUtil{
public static boolean isBlank(Long id) {
return id == null;
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public User getUserDetail(Long id) {
if(StrUtil.isBlank(id)) {
return null;
}
return userDao.selectUserById(id);
}
}
@RunWith(MockitoJUnitRunner.StrictStubs.class)
@PrepareForTest({StrUtil.class})// 添加此注解, 将静态工具类加载
public class UserServiceImplTest {
// @InjectMock 注入被测对象, 一般写在最前面
@InjectMock
UserServiceImpl userServiceImpl;
// @Mock 注入需要的依赖对象
@Mock
UserDao userDao;
// 执行测试之前执行
@Before
void setup() {
MockitoAnnotations.initMocks(this);
// mock 静态类
PowerMockito.mockStatic(StrUtil.class);
}
/*
* 测试: 获取用户详情
*/
@Test
void testGetUserDetail() {
// mock 一个 User 对象
User user = mock(User.class);
//断言
Assert.assertNotNull(user);
// 设置属性
user.setName("张三");
// 模拟 UserDao 查询返回结果,使用 anyLong()代表任意 Long 类型的参数. 模拟返回我们 mock 的 user 对象
when(userDao.selectUserById(anyLong())).thenReturn(user);
// 模拟判断返回 false
when(StrUtil.isBlank(anyLong())).thenReturn(false);
// 执行需要测试的方法
User userInfo = userServiceImpl.getUserDetail(anyLong());
// 断言:userInfo 不为 null
Assert.assertNotNull(userInfo);
// 断言:user.getName() 等于 userInfo.getName()
Assert.assertEquals(user.getName(), userInfo.getName())
}
}
测试抛出异常
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public User getUserDetail(Long id) {
if(StrUtil.isBlank(id)) {
throw new NullPointException();
}
return userDao.selectUserById(id);
}
}
@RunWith(MockitoJUnitRunner.StrictStubs.class)
@PrepareForTest({StrUtil.class})// 添加此注解, 将静态工具类加载
public class UserServiceImplTest {
// @InjectMock 注入被测对象, 一般写在最前面
@InjectMock
UserServiceImpl userServiceImpl;
// @Mock 注入需要的依赖对象
@Mock
UserDao userDao;
// 执行测试之前执行
@Before
void setup() {
MockitoAnnotations.initMocks(this);
// mock 静态类
PowerMockito.mockStatic(StrUtil.class);
}
/*
* 测试: 获取用户详情
*/
@Test
void testGetUserDetail() {
// mock 一个 User 对象
User user = mock(User.class);
//断言
Assert.assertNotNull(user);
// 设置属性
user.setName("张三");
// 模拟 UserDao 查询返回结果,使用 anyLong()代表任意 Long 类型的参数. 模拟返回我们 mock 的 user 对象
when(userDao.selectUserById(anyLong())).thenReturn(user);
// 模拟判断返回 false
when(StrUtil.isBlank(anyLong())).thenReturn(true);
// 模拟方法抛出异常
when(userServiceImpl.getUserDetail(anyLong())).thenThrows(new NullPointException());
User user = null;
try{
user = userServiceImpl.getUserDetail(anyLong());
}catch(Exception e) {
// 断言:抛出的异常类型是 NullPointException
Assert.assertTrue(e intanceof NullPointException)
}
Assert.assertNull(user);
}
}
使用 Mockito 进行复杂业务单测
本节演示如何使用 Mockito 对一些复杂的业务进行单测。很多时候,我们的功能业务也许不是以 RESTful 为单位的,不能直接使用接口调用测试。实际测试中,有很多难以使用接口调用的方式测到,往往是这些难以测试的部分导致了 Bug 的产生。
我们模拟一个转账场景:
@Service
public class UserService {
@Autowired
UserHandler userHandler;
public User getUserByPayId(Long payId) {
return userHandler.getUserByPayId(payId);
}
public User getUserByPayeeId(Long payeeId) {
return userHandler.getUserByPayeeId(payeeId);
}
}
@Component
public class UserHandler {
@Autowired
UserDao userDao;
@Autowired
AccountDao accountDao
public User selectUserByPayId(Long payId) {
Account account = accountDao.selectAccountById(payId);
if(null == account) {
return new NullPointException();
}
return userDao.selectUserById(account.getUserId());
}
public User selectUserByPayeeId(Long payeeId) {
Account account = accountDao.selectAccountByPayeeId(payeeId);
if(null == account) {
return new NullPointException();
}
return userDao.selectUserById(account.getUserId());
}
// ...
}
@Service
public class PermissionService {
@Autowired
PermissionHandler permissionHandler;
public boolean hasPermission(User user) {
return permissionHandler.hasPermission(user);
}
// ...
}
@Service
public class AccountService {
@Autowired
AccountHandler accountHandler;
// 转账
public boolean transfer(Long payId,Long payeeId, String payAmount) {
// ...
return true;
}
}
@Component
public class Transfer {
@Autowired
UserService userService;
@Autowired
AccountService accountService;
@Autowired
PermissionService permissionService;
public void transfer(Long payId,String payAmount, Long payeeId) throws Exception {
// 参数校验
if(null == payId||null == payeeId) {
throw new NullPointException();
}
if(StrUtil.isBlank(payAmount)) {
throw new TransferException("pay amount is null");
}
User payer = userService.getUserByPayId(payId);
User payee = userService.getUserByPayeeId(payId);
// 校验操作权限
if(!permissionService.hasPermission(payer) || !permissionService.hasPermission(payee)) {
throw new TransferException("Don't have operation permission");
}
// 核心转账
accountService.transfer(payId, payAmount, payeeId);
}
}
现在它们的依赖关系大致是这个样子的:
我们开始写单测。
// 第一: 创建测试类
// 第二: 使用@RunWith 启动
@RunWith(MockitoJUnitRunner.StrictStubs.class)
// 第三:首先加载静态工具类等
@PrepareForTest({StrUtil.class})
public class TransferTest {
// 第四:注入相应依赖, 依次注入
@InjectMock
Transfer transfer;
@Mock
UserService userService;
@Mock
AccountService accountService;
@Mock
PermissionService permissionService;
@Mock
UserHandler userHandler;
@Mock
AccountHandler accountHandler;
@Mock
PermissionHandler permissionHandler;
@Mock
UserDao userDao;
@Mock
AccountDao accountDao;
@Mock
PermissionDao permissionDao
// 第五:初始装载一些环境
@Before
void setup() {
// 初始化 mock
MockitoAnnotations.initMocks(this);
// mock 静态类
PowerMockito.mockStatic(StrUtil.class);
}
// 第六:创建测试方法, 添加测试注解
@Test
public void testTransferWhenSuccess() {
// 第七:模拟参数
Long payId = 1L;
String payAmount = 100;
Long payeeId = 1L;
// 第八:模拟静态 isBlank 返回结果
when(StrUtil.isBlank(anyString())).thenReturn(false);
// 第九:模拟查询结果
User payer = mock(User.class);
User payee = mock(User.class);
Assert.assertNotNull(payer);
Assert.assertNotNull(payee);
// 模拟 dao
when(userDao.selectUserById(anyLong())).thenReturn(payer);
when(userDao.selectUserById(anyLong())).thenReturn(payee);
// 模拟 handler
when(userHandler.selectUserByPayId(anyLong())).thenReturn(payer);
when(userHandler.selectUserByPayeeId(anyLong())).thenReturn(payee);
// 模拟 serivce
when(userService.getUserByPayId(anyLong())).thenReturn(payer);
when(userService.getUserByPayId(anyLong())).thenReturn(payee);
// 其他依赖一样如此模拟,重复代码就省略了...
// ....
// 第十:模拟权限校验通过
when(permissionService.hasPermission(payer))..thenReturn(true);
when(permissionService.hasPermission(payee))..thenReturn(true);
// 最后执行目标测试方法
accountService.transfer(payId, payAmount, payeeId);
}
}
我们可以清楚的发现,我们想要一步一步执行下去,就必须符合前一步的条件。但是很多时候我们是没有连接数据库,没有生产环境数据的,此时模拟结果返回就很实用了,这样我们就能很轻松的验证我们的业务逻辑了。
大厂单元测试实战经验总结分享
覆盖率
BAT 等公司是非常重视单测的,一般不论是分支代码,还是增量代码最低要求都是要覆盖达到 80%以上。覆盖率可以大大地减少一些低级错误和缺陷。
单测维护
当逻辑代码发生了变更,相应的单测也应该进行修改,还有涉及到改功能的单测也需要进行维护。当然,一般大公司都有自己的检测工具,可以检测到所有单测是否通过。只要有一个单测不通过,就不能发版。
注重细节
我们一直认为功能才是最重要的,往往忽略了单测,当然单测也是非常花时间的,几乎和写功能花费一样的时间。这时候就要看取舍了,想要精益求精,那么单测绝对是让你代码质量更上一层楼的保证。