近期在项目中有写一些单元测试,在这里总结一些比较好的实践,本文主要从代码方面来讲解。读者需要有单测一些理论的前置知识,比如说几个比较常见的原则,AIR 原则,边界,异常,空测试。
目前觉得比较有实践价值的有:
- 不要为了提升单测覆盖率去测 bean
- 使用接口隔离网络以及外部依赖环境
- 尽量使用 IOC 解耦,方便 mock 掉工具类
- 测试数据统一存放
- 尽量使用 ParameterizedTest 来化简代码
1. 不要为了提升单测覆盖率去测bean
有很多公司要求单测覆盖率,bean 有很多 get set 方法也被统计进去,不要为了覆盖率去给这些 get set 方法写单测,这样虽然单元测试的覆盖率上去了,但是程序仍然没有得到正确的测试,善用屏蔽。
IDEA:
屏蔽掉即可。
2. 隔离外部环境依赖
好的测试不需要依赖外部环境,依赖外部网络等环境可能会因为网络原因导致单元测试失败。
一般后端与外部系统交互有缓存,服务调用,数据库等。
用缓存举例,外部调用等建议抽象出一个统一的接口,通过 Spring 的 profile 来注入不同的 bean 这里拿缓存举例。
这里的 fake cache 可能是一个使用 Map 实现的本地缓存,不需要与外部系统进行交互。
比如说我们有一个系统需要与外部 Redis 做交互,Redis 只需要简单的 get set 操作,这时候我们抽象出一个接口:
/**
* 只提供简单的String,之间的get set操作 在不同的环境中注入不同的实现
*
* @author zjmeow
*/
public interface Cache {
/**
* 参考map中的put方法
*
* @param key 键
* @param value 值
*/
void put(String key, String value);
/**
* 参考map中的get方法
*
* @param key 键
* @return 值
*/
String get(String key);
}
然后实现一套用于测试的本地缓存
/**
* 实现一个本地缓存,本地运行以及单元测试使用这个缓存避免单测对网络环境的依赖
*
* @author zjmeow
*/
@Component
@Profile("test")
public class ProxyLocalCache implements Cache {
private Map<String, String> cache = new ConcurrentHashMap<>();
@Override
public void put(String key, String value) {
cache.put(key, value);
}
@Override
public String get(String key) {
return cache.get(key);
}
}
再实现一套与外部系统交互的缓存
/**
* 实现一个本地缓存,本地运行以及单元测试使用这个缓存避免单测对网络环境的依赖
*
* @author zjmeow
*/
@Component
@Profile("dev")
public class ProxyLocalCache implements Cache {
private Jedis jedis = new Jedis("...");
@Override
public void put(String key, String value) {
jedis.set(key, value);
}
@Override
public String get(String key) {
return jedis.get(key);
}
}
这样缓存就能避免外部环境的依赖了。
当然数据库最好使用 h2 内存数据库来避免外部环境的依赖。
3. 尽量使用 IOC
使用 IOC 可以解耦对象,使得测试更加方便。经常有这样的情况,在某个 service 中使用到某个工具类,这个工具类内的方法都是 static 的,这样的话,测试 service 的时候就会连着工具类一起测试了。如果使用 IOC 的话可以注入自己的实现。
假如说我们有一个工具类 TokenUtil,用于生成指定规则的随机字符串,如果直接在 Service 调用这个 Util 的静态方法,代码如下。
/**
* token 生成器
*
* @author zjmeow
*/
public class TokenUtil {
public static String getToken() {
// 虚假的token生成器
return Instant.now().getEpochSecond() + "";
}
}
/**
* token service
*
* @author zjmeow
*/
@Service
public class TokenService {
public String login(String username, String password) {
// 虚假的查询一下password
if (password.equals(queryPassword(username))) {
return TokenUtil.getToken();
}
return null;
}
}
当我们需要单元测试的时候,由于耦合度过高,就没办法单独的测试 TokenService 了。
而且当我们测试需要某个固定的 token 也是难以实现的,假如使用了依赖注入,那么事情就变得不一样了。
/**
* token 生成器
*
* @author zjmeow
*/
@Component
public class TokenUtil {
public String getToken() {
// 虚假的token生成器
return Instant.now().getEpochSecond() + "";
}
}
/**
* token service
*
* @author zjmeow
*/
@Service
public class TokenService {
private final TokenUtil tokenUtil;
@Autowired
public TokenService(TokenUtil tokenUtil) {
this.tokenUtil = tokenUtil;
}
public String login(String username, String password) {
// 虚假的查询一下password
if (password.equals(queryPassword(username))) {
return tokenUtil.getToken();
}
return null;
}
}
这时候想要单独的测试 TokenService 只需要很简单的 mock 一个 TokenUtil 就可以了,不仅隔离了 TokenUtil 和 TokenService,而且还能很方便的指定 TokenUtil 返回的对象。
/**
* token service test
*
* @author zjmeow
*/
class TokenServiceTest {
private TokenService tokenService;
private static final String FAKE_TOKEN = "123456789";
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
@BeforeEach
void setUp() {
TokenUtil util = mock(TokenUtil.class);
when(util.getToken()).thenReturn(FAKE_TOKEN);
tokenService = new TokenService(util);
}
@Test
void login() {
assertEquals(FAKE_TOKEN, tokenService.login(USERNAME, PASSWORD));
}
}
4. 测试数据
需要频繁使用到的数据可以考虑用一个包来统一放测试数据,这样能避免手滑导致的单测失败,同时也减少了维护成本,比如说我们需要一个测试用户,可以单独的写一个类出来。
package com.xxx.constant;
public class TestUser {
public static final String USERNAME = "test_user_name";
public static final String PASSWORD = "test_password";
}
这样就能方便在多个测试类中使用了。
像 Flink 等一些需要大量测试数据的地方,最好能单独写一个类到文件中读取数据流,方便在多个类中复用。
5. ParameterizedTest 的使用
灵活的使用 csv 可以减少很大一部分代码,可以在一套数据中同时测试到边界,空值等,假如说我们想要测试一个检测 ip 是否合规的函数,就可以用一组 CsvSource 来测试对空字符串,null 等进行测试
class IpUtilTest {
private IpUtil ipUtil = new IpUtil();
@ParameterizedTest
@CsvSource({
"192.168.1.1,true",
"aaaaaaa,false",// invalid ip
",false",// null
"'',false",// empty string
})
void check(String ip, boolean except) {
assertEquals(except, ipUtil.check(ip));
}
}
本文由 鸡米 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Oct 24,2020