单元测试(TestNG+PowerMock)
在程序开发完成后,我们往往不能保证程序 100%
的正确,通过单元测试的编写,我们可以通过自动化的测试程序将我们的输入输出程序进行定义,通过断言来 Check 各个 Case
的结果,检测我们的程序,以提高程序的正确性,稳定性,可靠性。
我们将使用TestNG+PowerMock来完成我们的单元测试
- TestNG:是一套根据 JUnit 思想构建,利用注释来强化测试功能的一个测试框架,即可以用来做单元测试,也可以用来做集成测试。
- Mockito:是一种单元测试模拟框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等
- PowerMock:是在单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 实现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持。
POM
<testng.version>6.9.10</testng.version><powermock.version>2.0.9</powermock.version><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>
</dependencies>
常用注解
TestNG 注解
- @BeforeClass 在调用当前类的第一个测试方法之前运行,注释方法仅运行一次
- @AftereClass 在调用当前类的第一个测试方法之后运行,注释方法仅运行一次
- @BeforeMethod 注释方法将在每个测试方法之前运行
- @AfterMethod 注释方法将在每个测试方法之后运行
- @BeforeTest 注释的方法将在属于test标签内的类的所有测试方法运行之前运行
- @AfterTest 注释的方法将在属于test标签内的类的所有测试方法运行之后运行
- @Test 将类或方法标记为测试的一部分
PowerMock 注解
- @Mock 创建一个模拟;
- @InjectMocks 创建该类的实例,并将使用@Mock(或@Spy)注释创建的模拟注入该实例
- @Spy 封装一个真实的对象,以便可以像其他 mock 的对象一样追踪、设置对象的行为;
- @PrepareForTest对于静态方法,私有方法,final 方法,在用powermock做单元测试的时候,需要增加注解;
使用
mock 对象
为对象加上一层代理
@Test
public void testTestWhen() {User user = new User();User userMock = PowerMockito.mock(User.class);
}

打桩
通过打桩,可以让被我们mock的对象,在调用指定方法的时候,按照我们的需要返回。
- 隔离:单测的时候需要保证我们测试的单元一定要独立,不能依赖于外部的方法或接口,单元中调用到的方法也需要有对应的单测来保证功能。
- 补齐:如果我们依赖的外部方法还没开发,或者是项目是并行开发,那此时打桩就可以把未完成的方法给代替掉。
- 控制:测试时,我们对外部方法的实现细节不需要去理解,就像个黑盒,但外部方法的返回结果可能跟我们测试结果有直接关系,那此时我们可以通过打桩,控制外部方法按要求返回我们希望的结果。
以下示例,得到结果并不会是真实的返回结果,而是mock对象的方法所提供的默认结果,默认值为 null
@Override
public String getTest() {// 1、xxx// 2、xxxreturn testDemoDao.getTestObj();
}
//-----------------
@InjectMocks
TestDemoServiceImpl testDemoService;
@Test
public void getTest() {String test = testDemoService.getTest();System.out.println(test);
}
//-------结果-------
null
通过打桩,我们可以实现对mock类方法自定义返回结果
@InjectMocks
TestDemoServiceImpl testDemoService;
@Mock
TestDemoDao testDemoDao;
@Test
public void testTestWhen() {PowerMockito.when(testDemoDao.getTestObj()).thenReturn("测试模拟数据");String test = testDemoService.getTest();System.out.println(test);
}
//-------结果-------
测试模拟数据
mock 静态方法
public class OnlineUserUtil {private final static ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();public static void set(UserInfo userInfo) {threadLocal.set(userInfo);}public static UserInfo get() {return threadLocal.get();}
}
@Test
public void orderCancel() {PowerMockito.mockStatic(OnlineUserUtil.class);UserInfo userInfo = new UserInfo();PowerMockito.when(OnlineUserUtil.get()).thenReturn(userInfo);
}
注意如果是静态方法需要在类上加注解@PrepareForTest注解
@PrepareForTest({DateUtils.class})
public class DemoImplTest extends PowerMockTestCase{...}
mock 私有方法
public class World {private Banana banana = new Banana();public Banana getBanana() {return banana;} private String secretGreeting(String s){return "i am secretly saying: " + s;}public String getLog(String s){return secretGreeting(s);}
}
@PrepareForTest({World.class})
public class WordTest extends PowerMockTestCase{@Mockprivate Banana banana;@InjectMocksprivate World world;@Testpublic void test1() throws Exception {// 用的是powermock的spyworld = PowerMockito.spy(world);// mock私有方法// 先when再thenReturn也行PowerMockito.doReturn("11111").when(world, "secretGreeting", "pp");System.out.println(world.getLog("pp"));// 自动注入仍然成功PowerMockito.when(banana.shout(Mockito.anyString())).thenReturn("22222");System.out.println(world.getBanana().shout("wow"));}
}
mock Void方法
public interface SmsService {void checkMsg(String code, String phone);
}
@PrepareForTest({World.class})
public class WordTest extends PowerMockTestCase{@InjectMocksprivate World world;@MockSmsService smsService;@Testpublic void test1() throws Exception {PowerMockito.doNothing().when(smsService).checkMsg(Mockito.any(),Mockito.any());}
}
Mock redisTemplate
@Resource
RedisTemplate<String,String> redisTemplate;public void getUser(String vipOrderNo) {Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(formatKey, "1");
}
@PrepareForTest(RedisTemplate.class)
public class UserImplTest extends PowerMockTestCase {@InjectMocksUserImpl user;@MockRedisTemplate<String,String> redisTemplate;@Testpublic void testGetUser() {ValueOperations<String, String> valueOperations= PowerMockito.mock(ValueOperations.class);PowerMockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);PowerMockito.when(valueOperations.setIfAbsent(Mockito.any(),Mockito.any())).thenReturn(true);user.getUser("A");}
}
mock 系统配置
User user = new User();
user .setMemo(sysConfig.get(SysConfigKey.USER_MEMO.getKey()));
@PrepareForTest({World.class})
public class WordTest extends PowerMockTestCase{@InjectMocksprivate World world;@MockSysConfig sysConfig;@Testpublic void test1() throws Exception {PowerMockito.when(sysConfig.get(SysConfigKey.USER_MEMO.getKey())).thenReturn("备注");}
}
mock @Value值
@Service
public class BillServiceImpl implements BillServic {@Value("${cash.bill.savePath}")private String billSavePath;@Overridepublic List<PlatformOrderBill> downloadBillFile(String checkDate) {File saveDir = new File(billSavePath);}}
@InjectMocks
BillServiceImpl billService;// 单元测试类里面加上这一行代码
ReflectionTestUtils.setField(billService, "billSavePath", "D://xxx.txt");
mock 异常
@Override
public String getTest() {try{testDemoDao.getTestObj();}catch(RuntimeException e){testDemoService.getTest();}
}
@Test
public void testTestWhen() {PowerMockito.when(testDemoDao.getTestObj()).thenThrow(new RuntimeException());
}
参数匹配器
如果只是用到上述打桩方式,一定程度上来说是不够灵活的,所以也就有了参数匹配器。
String getTestObj(String str);
//-----------------
@Test
public void testTestWhen() {PowerMockito.when(testDemoDao.getTestObj(Mockito.any())).thenReturn("BBB");String test = testDemoService.getTest();System.out.println(test);
}
//-------结果-------
BBB
验证是否执行了某些操作
public String getTest() {String str = testDemoDao.getTestObj("BBB");if (str.equals("BBB")){testDemoDao.getFailMsg();}return str;
}
//-----------------
@Test
public void testTestWhen() {PowerMockito.when(testDemoDao.getTestObj(Mockito.any())).thenReturn("CCC");testDemoService.getTest();Mockito.verify(testDemoDao).getFailMsg();
}
//-------结果-------
Wanted but not invoked:
testDemoDao.getFailMsg();
其他
在对MybatisPlus的baseMapper.selectList进行mock时遇到的问题。
mapper.selectList(new LambdaQueryWrapper<User>().eq(User::getName,"name").orderByAsc(User::getAge));
//-------结果-------
xxx.xxx.MybatisPlusException: can not find lambda cache for this entity。
原因是TableInfo的初始化过程被中止,或者被Mock替换了,根本就没有触发
解决方案
@BeforeTest
public void initTable() {TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(),""), User.class);
}
如果dao层实际执行sql语句,而不想要mock的话
如果想要走实际的sql语句,就需要注入对象,而我们的@Test注入对象,就需要加载ApplicationContext,启动spring容器。
@SpringBootTest(classes = ApiApplication.class)
public class DemoImplTest{
}
如果我们想要走实际sql,又不想对库造成影响,就需要对@Test进行回滚操作
@SpringBootTest(classes = ApiApplication.class)
public class DemoImplTest extends AbstractTransactionalTestNGSpringContextTests{@ResourceUserDaoImpl userDao;@Test@Rollback()//@Transactionalpublic void testSaveUser() {User user = new User;userDao.saveUser(user);}
}
如果是多模块项目的话需要将test类放到启动类在的包下
即使用容器对象又使用mock的方法
@SpringBootTest(classes = ApiApplication.class)
@TestExecutionListeners(MockitoTestExecutionListener.class)
public class ImgConfigServiceImplTest extends AbstractTransactionalTestNGSpringContextTests{@Resourceprivate ImgConfigServiceImpl service;@MockBeanSysConfig sysConfig;@Testpublic void testGetVipMainImg() {PowerMockito.when(sysConfig.get(SysConfigKey.VIP_MAIN_IMG.getKey())).thenReturn("测试用力会员卡主图");service.getVipMainImg();}
}
参考网址
TestNg官网:https://github.com/cbeust/testng/
Mockito官网:https://github.com/mockito/mockito
Powermock官网:https://github.com/powermock/powermock
Jacoco官网:https://github.com/jacoco/jacoco
Extentreports官网:https://www.extentreports.com/
