单元测试(Unit Testing),是指对软件或项目中最小可测试单元进行正确性检验的测试工作。单元是人为规定最小可测试的功能模块,可以是一个模块,一个函数或者一个类。单元测试需要与模块开发进行隔离情况下进行测试。
在程序开发完成后,我们往往不能保证程序 100% 的正确,通过单元测试的编写,我们可以通过自动化的测试程序将我们的输入输出程序进行定义,通过断言来 Check 各个 Case 的结果,检测我们的程序。以提高程序的正确性,稳定性,可靠性,节省程序开发时间。我们在项目中主要用到的单元测试框架有 Spring-Boot-Test
TestNG
、PowerMock
等。
TestNG
,即 Testing, Next Generation
,下一代测试技术,是一套根据 JUnit 和 NUnit 思想而构建的利用注释来强化测试功能的一个测试框架,即可以用来做单元测试,也可以用来做集成测试。
PowerMock
也是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock
现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。
常用注解
1. TestNG 注解
@BeforeSuite
在该套件的所有测试都运行在注释的方法之前,仅运行一次@AftereSuite
在该套件的所有测试都运行在注释方法之后,仅运行一次@BeforeClass
在调用当前类的第一个测试方法之前运行,注释方法仅运行一次@AftereClass
在调用当前类的第一个测试方法之后运行,注释方法仅运行一次@BeforeMethod
注释方法将在每个测试方法之前运行@AfterMethod
注释方法将在每个测试方法之后运行@BeforeTest
注释的方法将在属于test标签内的类的所有测试方法运行之前运行@AfterTest
注释的方法将在属于test标签内的类的所有测试方法运行之后运行@DataProvider
标记一种方法来提供测试方法的数据。注释方法必须返回一个Object [] []
,其中每个Object []
可以被分配给测试方法的参数列表。要从该DataProvider
接收数据的@Test
方法需要使用与此注释名称相等的dataProvider
名称@Parameters
描述如何将参数传递给@Test
方法 ;适用于 xml 方式的参数化方式传值@Test
将类或方法标记为测试的一部分,此标记若放在类上,则该类所有公共方法都将被作为测试方法
2. PowerMock 注解
@Mock
注解实际上是 Mockito.mock() 方法的缩写,我们只在测试类中使用它;@InjectMocks
主动将已存在的 mock 对象注入到 bean 中, 按名称注入, 但注入失败不会抛出异常;@Spy
封装一个真实的对象,以便可以像其他 mock 的对象一样追踪、设置对象的行为;
示例代码
1. 添加 pom.xml 依赖
以 Spring-Boot 项目为例,首先我们需要添加 TestNG
+ ProwerMock
依赖依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency><dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng.version}</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>${powermock.version}</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>${powermock.version}</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-testng</artifactId> <version>${powermock.version}</version> <scope>test</scope></dependency>
2. 增加单元测试
增加测试代码
import com.test.testng.dto.OrderDto;import com.test.testng.dto.UserDto;import org.mockito.*;import org.powermock.modules.testng.PowerMockTestCase;import org.testng.annotations.BeforeMethod;import org.testng.annotations.Test;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.when;public class OrderServiceTest extends PowerMockTestCase { @BeforeMethod public void before() { MockitoAnnotations.openMocks(this); } @InjectMocks private OrderService orderService; @Mock private UserService userService; // 正常测试 @Test public void testCreateOrder() { //1. mock method start UserDto userDto = new UserDto(); userDto.setId(100); when(userService.get()).thenReturn(userDto); //2. call business method OrderDto order = orderService.createOrder(new OrderDto()); //3. assert assertEquals(order.getId(), 100); } // 异常测试 @Test public void testCreateOrderEx() { //1. mock method start when(userService.get()).thenThrow(new RuntimeException()); Exception exception = null; try { //2. call business method orderService.createOrder(new OrderDto()); } catch (RuntimeException e) { exception = e; } //3. assert assertNotNull(exception); }}
常用 Mock 方式
1. Mock 静态方法
//静态方法UserDto dto = new UserDto();dto.setId(100000);PowerMockito.mockStatic(UserService.class);PowerMockito.when(UserService.loginStatic()).thenReturn(dto);UserDto userDto = UserService.loginStatic();assertEquals(100000, userDto.getId().intValue());
2. Mock 私有属性
//字段赋值ReflectionTestUtils.setField(orderService, "rateLimit", 99);
3. Mock 私有方法
// 模拟私有方法MemberModifier.stub(MemberMatcher.method(UserService.class, "get1")).toReturn(new UserDto());// 测试私有方法Method method = PowerMockito.method(UserService.class, "get1", Integer.class);Object userDto = method.invoke(userService, 1);assertTrue(userDto instanceof UserDto);
进阶使用
1. 参数化批量测试
在测试数据比较多的时候,我们可以通过 @DataProvider
生成数据源,通过 @Test(dataProvider = "xxx")
使用数据, 如下所示:
import com.test.testng.BaseTest;import com.test.testng.dto.UserDto;import org.mockito.InjectMocks;import org.testng.annotations.DataProvider;import org.testng.annotations.Test;import static org.testng.Assert.assertFalse;import static org.testng.AssertJUnit.assertTrue;public class UserServiceTest2 extends BaseTest { @InjectMocks private UserService userService; // 定义数据源 @DataProvider(name = "test") public static Object[][] userList() { UserDto dto1 = new UserDto(); UserDto dto2 = new UserDto(); dto2.setSex(1); UserDto dto3 = new UserDto(); dto3.setSex(1); dto3.setFlag(1); UserDto dto4 = new UserDto(); dto4.setSex(1); dto4.setFlag(1); dto4.setAge(1); return new Object[][] {{dto1, null}, {dto2, null}, {dto3, null}, {dto4, null}}; } // 正确场景 @Test public void testCheckEffectiveUser() { UserDto dto = new UserDto(); dto.setSex(1); dto.setFlag(1); dto.setAge(18); boolean result = userService.checkEffectiveUser(dto); assertTrue(result); } // 错误场景 @Test(dataProvider = "test") public void testCheckEffectiveUser(UserDto dto, Object object) { boolean result = userService.checkEffectiveUser(dto); assertFalse(result); } }
2. 复杂判断保证测试覆盖率
案例:
- 判断有效用户: 年龄大于 18 并且 sex = 1 并且 flag = 1
public boolean checkEffectiveUser(UserDto dto) { // 判断有效用户: 年龄大于 18 并且 sex = 1 并且 flag = 1 return Objects.equals(dto.getSex(), 1) && Objects.equals(dto.getFlag(), 1) && dto.getAge() != null && dto.getAge() >= 18;}
- 拆分逻辑。将其转换为最简单的 if ... else 语句。然后增加的单元测试,如下所示:
public boolean checkEffectiveUser(UserDto dto) { if (!Objects.equals(dto.getSex(), 1)) { return false; } if (!Objects.equals(dto.getFlag(), 1)) { return false; } if (dto.getAge() == null) { return false; } if (dto.getAge() < 18) { return false; } return true;}
- 拆分后我们可以看到,咱们只需要 5 条单元测试就能做到全覆盖。
public class UserServiceTest extends BaseTest { @InjectMocks private UserService userService; // 覆盖第一个 return @Test public void testCheckEffectiveUser_0() { UserDto dto =new UserDto(); boolean result = userService.checkEffectiveUser(dto); assertFalse(result); } // 覆盖第二个 return @Test public void testCheckEffectiveUser_1() { UserDto dto =new UserDto(); dto.setSex(1); boolean result = userService.checkEffectiveUser(dto); assertFalse(result); } // 覆盖第三个 return @Test public void testCheckEffectiveUser_2() { UserDto dto =new UserDto(); dto.setSex(1); dto.setFlag(1); boolean result = userService.checkEffectiveUser(dto); assertFalse(result); } // 覆盖第四个 return @Test public void testCheckEffectiveUser_3() { UserDto dto =new UserDto(); dto.setSex(1); dto.setFlag(1); dto.setAge(1); boolean result = userService.checkEffectiveUser(dto); assertFalse(result); } // 覆盖第五个 return @Test public void testCheckEffectiveUser_4() { UserDto dto =new UserDto(); dto.setSex(1); dto.setFlag(1); dto.setAge(18); boolean result = userService.checkEffectiveUser(dto); assertTrue(result); }}
- 单测覆盖率检测检测
35
publicbooleancheckEffectiveuser(userDtodto)
if(lobjects.eguals(dto.getsexO),b:1))
36
returnfalse;
37
38
b:1))f
if(lobjects.equals(dto.getFlag()
39
returnfaise;
40
41
if(dto.getageO)anu11)f
42
returnfaise;
43
]
44
(dto.getage()<18)
if
45
returnfaise;
46
47
returntrue;
48
49
3. 通过断言校验方法参数
assert
:断言是 java 的一个保留字,用来对程序进行调试,后接逻辑运算表达式,如下:
int a = 0, b = 1;assert a == 0 && b == 0;// 使用方法:javac编译源文件,再 java -ea class文件名即可。
- 在 Spring-Boot 中可以使用 Spring 提供的 Assert 类的方法对前端来的参数进行校验,如:
// 检查年龄 >= 18 岁public boolean checkUserAge(UserDto dto){ Assert.notNull(dto.getAge(), "用户年龄不能为空"); Assert.isTrue(dto.getAge() >= 18, "用户年龄不能小于 18 岁"); return Boolean.TRUE;}
- 如果是需要转换为,
rest api
返回的统一相应消息,我们可以通过:
@ControllerAdvicepublic class GlobalExceptionHandler { @ResponseBody @ExceptionHandler(value = IllegalArgumentException.class) public Response<String> handleArgError(IllegalArgumentException e){ return new Response().failure().message(e.getMessage()); }}
总结
原则上来讲,在功能模块的设计过程中我们应该遵循一下原则(参考 《软件工程-结构化设计准则》):
- 模块大小适中
- 合适的系统调用深度
- 多扇入、少扇出(增加复用度, 减少依赖程度)
- 单入口,单出口
- 模块的作用域,应该在模块内
- 功能应该可以预测的
- 高内聚,低耦合
- 系统分解有层次
- 较少的数据冗余