基本原则
单元测试,是指对程序中的最小可测试单元进行验证,在Java中的话,就是类。其有两个目的:
- 验证程序实现的逻辑是否与设计的逻辑正确
- 在涉及到代码修改时,用单元测试去保证原有功能不被破坏,
而一个好的单元测试应该具备以下FIRST 原则和AIR原则中的任何一条:
单元测试的FIRST 规则 - Fast 快速原则,测试的速度要比较快,- Independent 独立原则,每个测试用例应该互不影响,不依赖于外部资源。- Repeatable 可重复原则,同一个测试用例多次运行的结果应该是相同的- Self-validating 自我验证原则,单元测试可以自动验证,并不需要手工干预- Thorough 及时原则 单元测试必须即使进行编写,更新,维护。保证测试用例随着业务动态变化
AIR原则 - Automatic 自动化原则 单元测试应该是自动运行,自动校验,自动给出结果。- Independent 独立原则 单元测试应该独立运行,吧相互之间无依赖,对外无依赖,多次运行之间无依赖。- Repeatable 可重复原则 单元测试是可重复运动的,每次的结果都稳定可靠。
一个整套完善的单元测试可以保障后续的增添功能时,程序迭代过程中,代码的逻辑正确性。验证程序的输入和输出与最初设计一致。这对后续的集成测试等会提供巨大的帮助。同时也会有利于集成测试的顺利进行。
单元测试粒度
目前一些代码扫描工具基本都给出了最低30%的单元测试代码覆盖率。这是一个最低限度,然而一个项目的覆盖率,要综合去考虑项目成本,人员安排等等因素。
关于单元测试的粒度,可以总结以下几点:
- DAO层的单元测试: 对于基本CRUD,可以考虑跳过这一部分单元测试,而一些比较复杂的动态更新、查询等操作,建议用使用H2去做模拟单元测试。
- Service层的单元测试:基本上一个Service里面肯定会依赖很多其他的service(此处也建议将成员变量通过构造方法进行注入,以便于单元测试去Mock),此时建议我们将依赖其他service的方法用Mock替代,Service里面的一些数据库的操作也进行Mock。这样可以保证service测试的独立性,不过对于逻辑复杂的方法可能要花很多时间在Mock上面。 如果发现需要Mock的方法过多,那么可能就需要考虑将要测试的方法是不是需要重构。
- Controller(API)层的单元测试:主要着重测试HTTP status在 200,400,500 等情况下的异常处理,request及response的转换等。由于其余部分的代码测试都已经在其对应的单元测试覆盖,那么此时可以Mock绝大部分Serivce层中的方法。
- 一般工具类的单元测试:一些工具类里面包含了比较多的逻辑,所以需要尽可能考虑多种情况下测试用例。
单元测试示例
单元测试可使用的第三方工具非常多,然而我们不可能精通每一个,只有在不断 的使用提高熟练程度。 对于SpringBoot而言可以直接引入spring-boot-starter-test , 它将会引入JUnit、Spring Test、AssertJ、Hamcrest(匹配对象) Mockito、JSONassert、JsonPath等工具库。
- 如下是一个关于用户登录功能的测试示例
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class UserBizImplTest extends ParentTest {
@Resource
UserBiz userBiz;
@MockBean
UserService userService;
private static List<User> userList = new ArrayList<>();
@BeforeClass
public static void setUp() throws Exception {
System.out.println("setUp");
// mock userService
int code = 102;
for (int i = 0; i < 10; i++) {
User user = new User();
user.setId((long) i + 1);
user.setUsername("Tom" + code);
user.setPassword("P@ssw0rd");
userList.add(user);
code++;
}
}
@AfterClass
public static void tearDown() throws Exception {
System.out.println("tearDown");
}
@Test
public void test_08_login() {
// scenario 1: login success
// input data
LoginInfoDTO loginInfoDTO = new LoginInfoDTO();
loginInfoDTO.setPassword("P@ssw0rd");
loginInfoDTO.setUsername("admin");
// mock data & methods
String dbPassword = CommonUtils.encode(loginInfoDTO.getPassword());
User user = new User();
user.setUsername("admin");
user.setPassword(dbPassword);
Mockito.when(userService.getOne(Mockito.any())).thenReturn(user);
// verify
try {
String token = userBiz.login(loginInfoDTO);
System.out.println(token);
} catch (Exception e) {
TestCase.fail();
}
// scenario 2: user not exist
// input data
loginInfoDTO.setUsername("notExistedUser");
//mock data & methods
Mockito.when(userService.getOne(Mockito.any())).thenReturn(null);
// verify
try {
String token = userBiz.login(loginInfoDTO);
System.out.println(token);
TestCase.fail();
} catch (UserAuthorityException ignored) {
System.out.println(ignored);
} catch (Exception e) {
TestCase.fail();
}
// scenario 3: password error
// input data
loginInfoDTO.setUsername("admin");
loginInfoDTO.setPassword("wrongPassword");
//mock data & methods
Mockito.when(userService.getOne(Mockito.any())).thenReturn(user);
// verify
try {
String token = userBiz.login(loginInfoDTO);
System.out.println(token);
TestCase.fail();
} catch (UserAuthorityException ignored) {
System.out.println(ignored);
} catch (Exception e) {
TestCase.fail();
}
}
}
- 代码详解:
这段代码是一个单元测试类,用于测试UserBizImpl
类的登录功能。让我们逐步解释这段代码:
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
: 这是JUnit的一个注解,用于指定测试方法的执行顺序。在这里,它指定了按照方法名的字母顺序(升序)执行测试方法。public class UserBizImplTest extends ParentTest
: 这是测试类的定义,它继承了一个名为ParentTest
的父类。@Resource UserBiz userBiz
: 使用@Resource
注解注入了UserBiz
类的一个实例,即要被测试的业务类。@MockBean UserService userService
: 使用@MockBean
注解模拟了UserService
类的实例,这个类是UserBizImpl
的一个依赖。private static List<User> userList = new ArrayList<>();
: 定义了一个静态的User
对象列表,用于存储模拟数据。@BeforeClass public static void setUp() throws Exception
: 在所有测试方法运行之前执行的方法。在这里,它用于准备测试数据,模拟UserService
的行为。@AfterClass public static void tearDown() throws Exception
: 在所有测试方法运行之后执行的方法。在这里,它用于清理测试数据或资源。@Test public void test_08_login()
: 这是一个测试方法,用于测试用户登录功能。在
test_08_login
方法中:首先,定义了三个测试场景:
登录成功的情况
用户不存在的情况
密码错误的情况
对于每个场景,设置了输入数据(用户名和密码)和模拟的行为(使用
Mockito.when
方法模拟了UserService
的getOne
方法的返回值)。使用
userBiz
对象调用登录方法,并对返回结果进行验证。在成功登录和用户不存在的情况下,验证登录成功;在密码错误的情况下,验证抛出了UserAuthorityException
异常。
这段代码通过模拟UserService
的行为,测试了UserBizImpl
类的登录功能在不同场景下的行为。
参考
SpringBoot单元测试指南 - 知乎
版权归原作者 Benjamin1973 所有, 如有侵权,请联系我们删除。