软件测试之单元测试总结

🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快

一、何为单测

测试有黑盒测试和白盒测试之分,黑盒测试顾名思义就是我们不了解盒子的内部结构,我们通过文档或者对该功能的理解,指定了相应的输入参数,然后判断得出的结果是否正确。普通的用户、开发、QA都可以进行黑盒测试。

白盒测试与之相反,需要了解到内部的实现细节,一般是由开发人员自己来进行的,是基于对代码逻辑结构、各个关联方法了解基础上进行的。

白盒测试主要有 2 种

  • 静态代码分析:Findbugs、Sonarqube
  • 动态测试:单元测试

单元测试属于白盒测试里面的动态测试

二、单测的意义

2.1 解决问题的成本

测试金字塔,是单测中一张经典的图片。测试级别简单可以简单分为下面三类,详细的话可以归结为:单元测试、接口测试、集成测试、系统测试、验收测试。

如果发现问题,在金字塔越底层的阶段,解决问题的速度是越快的。

  • 本地开发环境发现问题:看几眼代码或者 debug 下就能定位出来了。
  • 生产环境发现问题:找日志(可能还没有输出),查数据库(可能没权限)。本地能复现的话还好,复现不了的话干着急。

2.2 维护系统的稳定性

我代码已经写好了挺久了,线上也运行一段时间了,还有必要补充单测吗?感觉单测写了一堆并没有发现问题,不知道价值点在哪。

  1. 校验你当前方法的正确性。
  2. 长时间保证你这个方法的稳定性,在往后需求的变更开发中,可能其他功能点影响到了这个方法,此时你的单测能很快帮你检查出来。
  3. 单测能够在你项目需要重构的时候,勇敢大步的往前走。与其反反复复修改问题,系统摇摇欲坠的,不如多花点时间优化代码,写写单测。

2.3 单测与持续集成的融合

单测 + CICD = 自动化测试

每次打包的时候自动跑单测用例,有问题快速反馈。没问题的代码才可以触发部署到对应的环境中。避免测试不足的代码提交到相关环境,导致服务用不了,测试人员一顿恼火。

三、单测拦路虎

1.框架繁多

新手对单测的框架没有意识,自己可能引用了一种, 其它框架 sdk 里面包含了另一种,比如 spring 的 framework 可能本身版本也五花八门,junit4 junit5 都有,使用的时候没注意乱用。

用了 junit5 写的用例,然后用 junit4 的 @Ignore 语法要去忽略这个单测,显然不行,因为在 junit5 对应的语法是 @Disabled

@Injectable @MockMethod @Mock @Test ... 迷茫

2.缺少理论的实践

3.对单测的理念认同不够,赶鸭子上架,内心其实是抵触的。

4.用例泛泛而写,没有遵守用例核心三步骤:mock 数据 -> 方法触发 -> 结果校验

5.为了覆盖率而写的单测

6.为了证明你方法是对的而写的单测(单测后面补的,单测里面方法触发了就算写完了,不是抱着验证的态度对所有结果进行充分细致的校验)

7.代码逻辑太复杂单测太难写

可以在写单测的过程,推动部分方法的重构。如果是新代码可以用 TDD(测试驱动开发) 理念,先写单测再写业务代码,这样实现起来的业务代码比较能做到高内聚,低耦合。

8.业务压力太大,单测太耗时

单测代码编写时间:业务代码编写时间 = 2~3:1,所以如果公司决定了写单测就同时也要给与这部分的时间。不能即催着业务上线,又催着单测达标,特别是前期在对单测还不够熟悉的基础上。

9.单测维护成本

单测也是需要维护的,case 多了后会发现,一有业务调整,不单单业务代码要调,单测也要调整,否则 case 会失败。

四、单测框架说明

1.Jmock

  • 网站:jMock - An Expressive Mock Object Library for Java
  • 不能对静态方法Mock

2.Mockito

3.JMockit

4.EasyMock

5.testNg

  • TestNG可以进行单元测试,功能测试,端到端测试,集成测试等。
  • 需要一个额外的xml配置文件,配置测试的class、method甚至package

6.Spock

7.TestableMock

8.junit4

  • Java领域内最为流行的单元测试框架

9.junit5

  • JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
  • JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
  • JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
  • JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。

10.Springtest

https://www.cnblogs.com/itplay/p/10101260.html

兼容多种测试引擎,便捷傻瓜,但是有下面几个问题

  • 可能存在多次启动 spring 容器
  • 一次 case 可能加载很多不需要的 bean ,导致速度很慢

11.总结

框架非常多,选择框架的时候可以从下面几个点考虑:

12.语法好写吗,文档全吗

13.能 mock 静态类,静态方法吗

  • 推荐 = junit5 + ( jmockit | testableMock),有冒险精神的可以尝试 Spock,速度快但是静态方法的 mock 需要借助其它工具。

Fastjunit = junit5 + jmockit + 测试工具集

单测框架总类繁多,本人很多都没有了解到位,以上总结仅为一家之言,兼听则明。

五、最佳实践

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 其它无用的东西(因为比较多)。此时为了让我们的单测好写,可以反向推动业务代码朝着高内聚低耦合的方向重构。

下面红框中的逻辑可以抽出来,主流程就清晰很多,用例也好写很多。

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.4造数据

你还在一个个属性的添加吗?

复制代码
@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 对象,自动的根据字段属性帮你随机产生相关数据。也支持数组对象的随机生成。可以节约不少时间。

5.5参数化测试

多种分支场景,使用参数化的测试可以让你的用例更简单。

复制代码
@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.6 数据库测试 - H2

H2 是一个内存数据,H2 仅仅只支持简单标准的 SQL 语法,如果各厂商特有的数据库引擎的特殊函数,可以使用 H2Function 扩展。

Fastjunit 同样对 H2 进行了一些封装:http://fastjunit.kubeclub.cn/db/h2/

5.7 并行测试

CICD 融入单测的过程,可能导致构建速度变慢,此时如果你的测试是并行的话,能在一定程度提高执行的速度。

5.8 IDEA 快捷键

多了解些快捷键,在单测的过程中执行一些批量操作还是挺有效率的。如 bean 十几个、几十个属性,要批量赋值,批量校验的一些场景。

5.9 单测的范围

5.10 单测报告 - Jacoco

http://fastjunit.kubeclub.cn/test-basic/jacoco-report/

六、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; }};
   }
}
  1. 实例化和属性注入:@Tested 自动实例化 ServiceAbc 对象,并把 @Injectable DependencyXyz 属性自动注入到 tested 里面。

  2. 模拟期望:Expectations 内部的匿名方法会实现对象的模拟和期望。

    // anyInstance 对象的 doSomething 方法被调用的时候将返回 123
    // 收到的参数需要是任意的字符类型 anyString ,万一收到一个 int,就不会返回 123 了
    anyInstance.doSomething(anyString); result = 123;

6.2 @Capturing

@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 参数的灵活匹配

在录制和验证阶段,一个对模拟方法或构造方法的调用参数做灵活的匹配。

1.any

最不严格的参数匹配,当然每个方法的参数都有类型的,还是要给定个恰当的参数类型。

复制代码
new Expectations() {{
   abc.voidMethod(anyString, (List&lt;?&gt;) any);
}};

2.with

复制代码
// 不为空即可
abc.voidMethod("str", (List&lt;?&gt;) 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);
}};

七、结尾

单测相关的意义开头已经讲了,这边不重复总结,补充下下面 2 点。

1.麻烦事

  • 业务代码的改动单测要跟着改动,不要之前写了单测,后面业务调整导致已有单测失败了,就简单的给 ignore 了。
  • 要一开始就规划好单测,不要写好业务代码,后续补单测。已经稳定的代码,补单测的意义不大。

2.收获

  • 提升开发素养:不做 CRUD 程序员,单测的工作中会让你深入了解各个代码甚至中间件的实现逻辑,深入底层了解内部实现。

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。

相关推荐
轻抚酸~4 小时前
KNN(K近邻算法)-python实现
python·算法·近邻算法
测试界的海飞丝5 小时前
10道软件测试面试题及其答案:
服务器·测试工具·职场和发展
独行soc5 小时前
2025年渗透测试面试题总结-264(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
汤姆yu6 小时前
基于python的外卖配送及数据分析系统
开发语言·python·外卖分析
如何原谅奋力过但无声6 小时前
TensorFlow 1.x常用函数总结(持续更新)
人工智能·python·tensorflow
翔云 OCR API6 小时前
人脸识别API开发者对接代码示例
开发语言·人工智能·python·计算机视觉·ocr
REDcker6 小时前
tcpdump 网络数据包分析工具完整教程
网络·测试工具·tcpdump
小白程序员成长日记6 小时前
2025.11.24 力扣每日一题
算法·leetcode·职场和发展
AndrewHZ7 小时前
【图像处理基石】如何在图像中提取出基本形状,比如圆形,椭圆,方形等等?
图像处理·python·算法·计算机视觉·cv·形状提取
温轻舟8 小时前
Python自动办公工具05-Word表中相同内容的单元格自动合并
开发语言·python·word·自动化办公·温轻舟