笔者的文章同时发布于 kubeclub
云原生技术社区,一个分享云原生生产经验,同时提供技术问答的平台,前往查看
测试有黑盒测试和白盒测试之分,黑盒测试顾名思义就是我们不了解盒子的内部结构,我们通过文档或者对该功能的理解,指定了相应的输入参数,然后判断得出的结果是否正确。普通的用户、开发、QA都可以进行黑盒测试。
白盒测试与之相反,需要了解到内部的实现细节,一般是由开发人员自己来进行的,是基于对代码逻辑结构、各个关联方法了解基础上进行的。
白盒测试主要有 2 种
单元测试属于白盒测试里面的动态测试
测试金字塔,是单测中一张经典的图片。测试级别简单可以简单分为下面三类,详细的话可以归结为:单元测试、接口测试、集成测试、系统测试、验收测试。
如果发现问题,在金字塔越底层的阶段,解决问题的速度是越快的。
我代码已经写好了挺久了,线上也运行一段时间了,还有必要补充单测吗?感觉单测写了一堆并没有发现问题,不知道价值点在哪。
单测 + CICD = 自动化测试
每次打包的时候自动跑单测用例,有问题快速反馈。没问题的代码才可以触发部署到对应的环境中。避免测试不足的代码提交到相关环境,导致服务用不了,测试人员一顿恼火。
用了 junit5 写的用例,然后用 junit4 的 @Ignore 语法要去忽略这个单测,显然不行,因为在 junit5 对应的语法是 @Disabled
@Injectable @MockMethod @Mock @Test … 迷茫
Fastjunit = junit5 + jmockit + 测试工具集
另外,作者开源的单测工具:fastjunit ,主要是在主流测试引擎的基础上扩展了一些工具方法。不强推,但是可以学习参考下。
单测框架总类繁多,本人很多都没有了解到位,以上总结仅为一家之言,兼听则明。
笔者的文章同时发布于 kubeclub
云原生技术社区,一个分享云原生生产经验,同时提供技术问答的平台,前往查看
- 用例要轻量,执行速度要够快
- 执行过后没有痕迹
- 不依赖特点环境,随处都可以执行
- 校验要全面
单测的代码跟业务代码一样,需要易于阅读,方便维护。
再复杂的用例都要清晰得看出下面 3 个步骤
1. 上下文设置:参数模拟,mock 无用服务
2. 触发测试用例执行
3. 结果断言
/**
* Given 给定上下文【初始化数据,Mock 外部调用】
*/
new Expectations(EsClient.class) {
{
EsClient.createDoc(withInstanceOf(SimpleDocVo.class), withInstanceOf(PipelineJobJunit.class));
result = "{}";
times = 1;
}
};
/**
* 执行测试代码
*/
RestResponse restResponse = callBackController.junitCallBak(jenkinsJunitVo);
/**
* Assert 要足够细致
*/
Assertions.assertThat(restResponse).hasFieldOrPropertyWithValue("code", 0);
好的代码编写测试用例的时候是比较顺畅的,如果写单测的时候觉得目标代码很难测试,这时候大概率是目标代码编写不合理,需要优化重构下。另一方面,如果在写业务代码的时候先写好单测框架,此时能反向推动你写成比较好的代码。
业务逻辑平铺在一个方法里面,此时你的单测不好关注主流程,也很难 mock 其它无用的东西(因为比较多)。此时为了让我们的单测好写,可以反向推动业务代码朝着高内聚低耦合的方向重构。
下面红框中的逻辑可以抽出来,主流程就清晰很多,用例也好写很多。
此方法里读取当前系统时间并根据该值返回结果。Datetime.now 是一个隐藏的动态变量,整个方法的输出结果依赖于 datetime 的时间。
public static string GetTimeOfDay()
{
DateTime time = DateTime.Now;
if (time.Hour >= 0 && time.Hour < 6>= 6 && time.Hour < 12>= 12 && time.Hour < 18 xss=removed>= 0 && dateTime.Hour < 6>= 6 && dateTime.Hour < 12>= 12 && dateTime.Hour < 18 xss=removed xss=removed xss=removed> StringUtil.isNotEmpty(simpleDocVo.getId()));
Assertions.assertThat(document)
.hasFieldOrPropertyWithValue("pipelineJobId", jenkinsJunitVo.getUapJobId())
.hasFieldOrPropertyWithValue("status", jenkinsJunitVo.getStatus())
.hasFieldOrPropertyWithValue("allCoverage", jenkinsJunitVo.getAllCoverage())
.hasFieldOrPropertyWithValue("newCoverage", jenkinsJunitVo.getNewCoverage())
.hasFieldOrPropertyWithValue("testRun", jenkinsJunitVo.getTestRun())
.hasFieldOrPropertyWithValue("testFailure", jenkinsJunitVo.getTestFailure())
.hasFieldOrPropertyWithValue("testSkipped", jenkinsJunitVo.getTestSkipped());
}
};
你还在一个个属性的添加吗?
@Test
public void webhookTestWebhook() {
OtptestWebhookQueryDTO dto = new OtptestWebhookQueryDTO();
dto.setApp("uap");
dto.setEnv("test");
dto.setJobId("xxx");
dto.setVersion("v2.2");
xxx
}
http://fastjunit.kubeclub.cn/test-basic/dataProvider/
Fastjunit 的数据生成器,任意给个 Bean 对象,自动的根据字段属性帮你随机产生相关数据。也支持数组对象的随机生成。可以节约不少时间。
多种分支场景,使用参数化的测试可以让你的用例更简单。
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
"0, 1, 1",
"1, 2, 3",
"49, 51, 100",
"1, 100, 101"
})
void add(int first, int second, int expectedResult) {
Calculator calculator = new Calculator();
assertEquals(expectedResult, calculator.add(first, second),
() -> first + " + " + second + " should equal " + expectedResult);
}
H2 是一个内存数据,H2 仅仅只支持简单标准的 SQL 语法,如果各厂商特有的数据库引擎的特殊函数,可以使用 H2Function 扩展。
Fastjunit 同样对 H2 进行了一些封装:http://fastjunit.kubeclub.cn/db/h2/
CICD 融入单测的过程,可能导致构建速度变慢,此时如果你的测试是并行的话,能在一定程度提高执行的速度。
多了解些快捷键,在单测的过程中执行一些批量操作还是挺有效率的。如 bean 十几个、几十个属性,要批量赋值,批量校验的一些场景。
http://fastjunit.kubeclub.cn/test-basic/jacoco-report/
class ExampleTest {
@Tested ServiceAbc tested;
@Injectable DependencyXyz mockXyz;
@Test
void doOperationAbc(@Mocked AnotherDependency anyInstance) {
new Expectations() {{
anyInstance.doSomething(anyString); result = 123;
AnotherDependency.someStaticMethod(); result = new IOException();
}};
tested.doOperationAbc("some data");
new Verifications() {{ mockXyz.complexOperation(true, anyInt, null); times = 1; }};
}
}
// anyInstance 对象的 doSomething 方法被调用的时候将返回 123
// 收到的参数需要是任意的字符类型 anyString ,万一收到一个 int,就不会返回 123 了
anyInstance.doSomething(anyString); result = 123;
@Mocked 一般是 mock 具体的对象,像一些接口或者基类,我们只知道具体的实现类,这种场景可以用 @Capturing。(例如:像一些权限校验,AOP 代理自动生成的场景)
//权限类,校验用户没有权限访问某资源
public interface IPrivilege {
/**
* 判断用户有没有权限
* @param userId
* @return 有权限,就返回true,否则返回false
*/
public boolean isAllow(long userId);
}
@Test
public void testCaputring(@Capturing IPrivilege privilegeManager) {
// 加上了JMockit的API @Capturing,
// JMockit会帮我们实例化这个对象,它除了具有@Mocked的特点,还能影响它的子类/实现类
new Expectations() {
{
// 对IPrivilege的所有实现类录制,假设测试用户有权限
privilegeManager.isAllow(testUserId);
result = true;
}
};
// 不管权限校验的实现类是哪个,这个测试用户都有权限
Assert.assertTrue(privilegeManager1.isAllow(testUserId));
Assert.assertTrue(privilegeManager2.isAllow(testUserId));
}
在录制和验证阶段,一个对模拟方法或构造方法的调用参数做灵活的匹配。
new Expectations() {{
abc.voidMethod(anyString, (List<?>) any);
}};
// 不为空即可
abc.voidMethod("str", (List<?>) withNotNull());
// 需要是什么类型,需要包含 xyz 字符
abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
// 前缀需要是 abc
mock.doSomething(anyInt, true, withPrefix("abc"));
// 更多查看接口文档
// 该方法最少被调用 2 次
abc.voidMethod(); minTimes = 2;
// 被调用 1~5 次
abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;
// 最多被调用 1 次
abc.anotherVoidMethod(3); maxTimes = 1;
new Verifications() {{
double d;
String s;
mock.doSomething(d = withCapture(), null, s = withCapture());
assertTrue(d > 0.0);
assertTrue(s.length() > 1);
}};
更多查看 api:http://jmockit.github.io/tutorial/Mocking.html
8 个基础注解的语法一定要完全了解:https://www.jianshu.com/p/6a59ea365648
单测相关的意义开头已经讲了,这边不重复总结,补充下下面 2 点。