大厂技术专家手把手教你如何写好单元测试

createh51个月前 (02-01)技术教程16

前言

说起单元测试,很多小公司不注重单元测试,从而导致了上线后出现各类奇怪问题,很大一部分开发人员都没有编写单元测试的习惯,但去了大厂之后才知道差距在哪里!单测是系统稳定的保障,不容不引起重视,写好单测是优秀的程序员的必备技能。本期给大家带来以下几个部分:

  • 单元测试基本概念讲解
  • 常用单元测试框架讲解
  • 单元测试规范及需要注意的内容
  • 实战代码讲解各类情况单元测试
  • 使用 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%以上。覆盖率可以大大地减少一些低级错误和缺陷。

单测维护

当逻辑代码发生了变更,相应的单测也应该进行修改,还有涉及到改功能的单测也需要进行维护。当然,一般大公司都有自己的检测工具,可以检测到所有单测是否通过。只要有一个单测不通过,就不能发版。

注重细节

我们一直认为功能才是最重要的,往往忽略了单测,当然单测也是非常花时间的,几乎和写功能花费一样的时间。这时候就要看取舍了,想要精益求精,那么单测绝对是让你代码质量更上一层楼的保证。

相关文章

java读写xml文件(java读取xml工具类)

1.读取xml文件文件格式如下: 张无忌 男 光明顶 minmin 3 4...

java实现文件上传到服务器(java实现文件上传的三种方式)

java实现文件上传到服务器,java实现大文件上传,java实现大文件分块上传,java实现大文件分片上传,java实现大文件切片上传,java实现大文件批量上传,java实现大文件加密上传,jav...

Java中excel文件解析总结以及超大文件读写的分析报告

在系统与系统之间进行数据传递时,经常需要使用Excel文件来进行数据的导入或者导出。因此,在Java语言实现这类需求时,往往会面临着数据的导入(解析)或者导出(生成)。Java中可以用来处理Excel...

一文了解 DataLeap 中的 Notebook

一、概述Notebook 是一种支持 REPL 模式的开发环境。所谓「REPL」,即「读取-求值-输出」循环:输入一段代码,立刻得到相应的结果,并继续等待下一次输入。它通常使得探索性的开发和调试更加便...

package-info.java 的使用(package java.util)

package-info.java 介绍pacakge-info.java 是一个 Java 文件,目标是提供一个包级的文档说明及包级的注释。在 Java 5 之前,包级的文档是 package.ht...

Java原生代码实现爬虫(爬取小说)(java开源爬虫平台)

Java也能做爬虫。现在提到爬虫人第一个想到的就是python,其实使用Java编写爬虫也是很好的选择,Java成熟的爬虫框架很多,下面给大家展示一个使用Java基础语言编写的爬取小说的案例:实现功能...