详细 - 单元测试
1.什么是单测
测试可分为黑盒测试和白盒测试。 顾名思义,黑盒测试就是我们不了解盒子的内部结构。 我们通过文档或者对函数的理解来指定相应的输入参数,然后判断结果是否正确。 . 普通用户、开发者、QA都可以进行黑盒测试。
白盒测试则相反,需要了解内部实现细节,这通常是开发人员在了解代码的逻辑结构和各个关联方法的基础上自己进行的。
白盒测试有两种主要类型
静态代码分析:Findbugs、Sonarqube 动态测试:单元测试
单元测试属于白盒测试中的动态测试
2. 单测的意义 2.1 解决问题的代价
测试金字塔是单机测试中的经典图片。 测试级别简单,可以简单分为以下三类。 详细来说可以概括为:单元测试、接口测试、集成测试、系统测试、验收测试。
如果发现问题,金字塔的层级越低,解决问题的速度就越快。
2.2 维护系统的稳定性
我的代码写了很久,网上跑了一段时间。 有必要加单测吗? 感觉写了一堆单测也没发现问题。 我不知道价值在哪里。
验证您当前方法的正确性。 确保您的方法长期稳定。 在以后开发需求的变化中,其他的功能点可能会影响到这个方法。 这时候你的单元测试就可以很快帮你查出来了。 当您的项目需要重构时,单一测试可以大步向前。
与其反复修改问题导致系统摇摇欲坠,不如花更多的时间优化代码和编写单元测试。
2.3 单元测试与持续集成的融合
单测+CICD=自动化测试
每次打包自动运行单个测试用例,有问题快速反馈。 没有问题代码可以触发部署到相应的环境。 避免向相关环境提交未充分测试的代码,导致服务无法使用和惹恼测试人员。
三、单一测试绊脚石
许多框架
新手不知道单测框架。 他们可能引用了一个,而其他框架 sdk 包含另一个。 比如spring的框架可能有各种版本,有junit4和junit5。
使用junit5写的用例BTC单元测试,然后使用junit4的@Ignore语法忽略这个单测,显然不行,因为junit5中对应的语法是@Disabled
@Injectable @MockMethod @Mock @Test ...困惑
没有理论的实践
对单体测试的概念认识不够,内心其实很矛盾赶鸭子上架。用例写的笼统,没有遵循用例的三个核心步骤:模拟数据 -> 方法触发 -> 结果验证。 为覆盖率编写的单个测试。 写的单测证明你的方法是正确的(单测后补充,如果触发了单元测试中的方法,即使写了,也不是以验证的态度去全面仔细地验证所有的结果) 代码逻辑太复杂,单元测试太难写
在编写单元测试的过程中,可以促进一些方法的重构。 如果是新代码,可以使用TDD(Test Driven Development)理念,先写单元测试再写业务代码,这样实现的业务代码可以做到高内聚低耦合。压力太大,单考太费时间
单元测试代码编写时间:业务代码编写时间=2~3:1,所以如果公司决定写单元测试,也必须给这部分时间。 不能同时督促业务上线,督促单测达标,尤其是在前期对单测不够熟悉的基础上。单测维护成本
单元测试也需要维护。 当案例太多的时候,你会发现,一旦有业务调整,不仅业务代码要调整,单元测试也要调整,否则案例会失败。 四、Jmock单机测试框架说明
网站:jMock - 一个富有表现力的 Java 模拟对象库
不能为静态方法 MockMockito
官方主页
不能为静态方法 MockJMockit
功能强大,可以mock静态方法 EasyMock
官方主页testNg
TestNG可以做单元测试、功能测试、端到端测试、集成测试等。
需要额外的xml配置文件来配置测试类、方法甚至packageSpock
BDD的单测写法和Groovy语法虽然速度很快,但是和我们java的编码习惯不太一样。 不能模拟静态方法。 可测试模拟
文档:#/zh-cn/doc/setup
TestableMock和JMockit的底层是一样的,都是采用“运行时修改字节码”的技术。 单元测试开始时,扫描测试类和被测类的字节码,完成Mock方法的替换。 junit4
Java领域最流行的单元测试框架junit5
JUnit 5 = JUnit 平台 + JUnit Jupiter + JUnit Vintage
JUnit Platform:Junit Platform是在JVM上启动测试框架的基础。 不仅支持JUnit自制测试引擎,还可以接入其他测试引擎。
JUnit Jupiter:JUnit Jupiter提供了JUnit5新的编程模型,是JUnit5新特性的核心。 内部包含一个测试引擎,用于在 Junit 平台上运行。
JUnit Vintage:由于JUint已经发展了很多年,为了照顾老项目,JUnit Vintage提供了兼容JUnit4.x和Junit3.x的测试引擎。
弹簧测试
兼容多种测试引擎,方便傻瓜,但存在以下问题
总结
有这么多框架。 在选择框架时,可以考虑以下几点:
语法好写吗,文档是否齐全? 你能模拟静态类和静态方法吗?
推荐=junit5+(jmockit | testableMock),如果你喜欢冒险,可以试试Spock,速度很快但是静态方法mock需要借助其他工具。
Fastjunit = junit5 + jmockit + 测试工具集
另外,作者开源的单机测试工具:fastjunit,主要是在主流测试引擎的基础上扩展了一些工具和方法。 不推荐,但可以借鉴。
单测框架有很多种,很多都没有被了解。 以上总结只是一个家庭的意见。
5. 最佳实践 5.1。 理论知识要牢记
- 用例要轻量,执行速度要够快
- 执行过后没有痕迹
- 不依赖特点环境,随处都可以执行
- 校验要全面
5.2. 测试代码模板
单元测试代码和业务代码一样,需要易读易维护。
再复杂的用例都要清晰得看出下面 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);
5.3 TDD测试驱动开发
好的代码在写测试用例的时候是比较顺利的。 如果在写单元测试的时候目标代码很难测试,很大概率是目标代码写的不合理,需要优化重构。 另一方面,如果你在写业务代码的时候先写单元测试框架,这时候可以反向推动你写出更好的代码。
5.3.1 松散代码
业务逻辑平铺在一个方法里面,此时你的单测不好关注主流程,也很难 mock 其它无用的东西(因为比较多)。此时为了让我们的单测好写,可以反向推动业务代码朝着高内聚低耦合的方向重构。
下图红框内的逻辑可以提炼出来BTC单元测试,主要流程清晰多了,用例也好写多了。
5.3.2 不稳定代码
此方法里读取当前系统时间并根据该值返回结果。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());
}
};
5.7 创建数据
你还在一个一个的添加属性吗?
@Test
public void webhookTestWebhook() {
OtptestWebhookQueryDTO dto = new OtptestWebhookQueryDTO();
dto.setApp("uap");
dto.setEnv("test");
dto.setJobId("xxx");
dto.setVersion("v2.2");
xxx
}
Fastjunit的数据生成器,给定任意一个Bean对象,根据字段属性自动为你随机生成相关数据。 还支持随机生成数组对象。 可以节省很多时间。
5.8 参数测试
多种分支场景,使用参数化测试可以让你的用例更简单。
@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);
}
5.9 数据库测试-H2
H2 是内存中的数据。 H2 只支持简单标准的 SQL 语法。 如果每个厂商都有特定的数据库引擎的特殊函数,可以使用H2Function扩展。
Fastjunit也封装了H2:
5.10 平行测试
将 CICD 集成到单个测试过程中可能会减慢构建速度。 这时候如果你的测试是并行的,执行速度可以得到一定程度的提升。
5.11 IDEA 快捷键
多了解一下快捷键,在单机测试的过程中进行一些批量操作还是比较高效的。 比如bean的属性有十几个或者几十个,有些场景需要批量赋值,批量校验。
5.12 单一测试量程
5.13单元测试报告-Jacoco
6. Jmockit 6.1 实例简介
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; }};
}
}
实例化和属性注入:@Tested自动实例化ServiceAbc对象,并自动将@InjectableDependencyXyz属性注入tested。
模拟期望:Expectations 中的匿名方法实现对象模拟和期望。
// anyInstance 对象的 doSomething 方法被调用的时候将返回 123
// 收到的参数需要是任意的字符类型 anyString ,万一收到一个 int,就不会返回 123 了
anyInstance.doSomething(anyString); result = 123;
6.2 @捕获
@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));
}
6.3 参数灵活匹配
在记录验证阶段,灵活匹配mock方法或构造方法的调用参数。
任何
最不严格的参数匹配。 当然,每个方法的参数都是有类型的,必须给出合适的参数类型。
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"));
// 更多查看接口文档
6.4 调用计数约束/验证
// 该方法最少被调用 2 次
abc.voidMethod(); minTimes = 2;
// 被调用 1~5 次
abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;
// 最多被调用 1 次
abc.anotherVoidMethod(3); maxTimes = 1;
6.5 从调用方法中捕获参数并进一步验证参数
new Verifications() {{
double d;
String s;
mock.doSomething(d = withCapture(), null, s = withCapture());
assertTrue(d > 0.0);
assertTrue(s.length() > 1);
}};
更多查看接口:
必须完全理解8个基本注解的语法:
七、结束
单测的意义在开头已经说了,这里不再重复总结,补充以下2点。
麻烦
报酬