写点什么

技术分享 | Javaer 如何做单元测试?

作者:LigaAI
  • 2022 年 5 月 27 日
  • 本文字数:8188 字

    阅读完需:约 27 分钟

技术分享 | Javaer 如何做单元测试?

前言:

本文适用于 javaer,其他开发者或许可以借鉴。

写本文的主旨有两个,一是简单的给大家介绍下单元测试,二是通过一个简单的示例来介绍一些单元测试的技巧,希望以此来降低大家写单元测试的门槛。


1、单元测试的定义



单元测试通常是由软件开发人员编写和运行的自动化测试,以确保应用程序的一部分(称为“单元”)符合其设计并按预期运行。在过程编程中,一个单元可以是一个完整的模块,但更常见的是一个单独的函数或过程。在面向对象编程中,一个单元通常是一个完整的接口,例如一个类,或者一个单独的方法。通过首先为最小的可测试单元编写测试,然后是它们之间的复合行为,可以为复杂的应用程序构建全面的测试。


Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the "unit") meets its design and behaves as intended. In procedural programming, a unit could be an entire module, but it is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, or an individual method. By writing tests first for the smallest testable units, then the compound behaviors between those, one can build up comprehensive tests for complex applications.


——Wikipedia, Unit testing


简单来说,单元测试是针对一个单元编写测试方法。其中的单元可以是一个很单纯的函数,也可以是一个完整的接口,该接口中可以包含各种其他函数的调用。


2、单元测试示例



该项目的 SpringBoot 版本是 2.2.5.RELEASE。


<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starters</artifactId> <version>2.2.5.RELEASE</version></parent>
复制代码


➮ 2-1.项目文件准备


假设有项目文件的目录结构如下:


其中依赖关系为:

java-study-web-provider 依赖 java-study-web-api, java-study-common-providerjava-study-web-api 依赖 java-study-common-apijava-study-common-provider 依赖 java-study-web-api, java-study-common-api
复制代码

在 java-study-web-api 包中有个 rpc 包,其中有两个 rpc 接口,分别是

WebRpc.class & WebRpc2.class。然而,这两个接口的实现类在 java-study-web-provider 包中。


public interface WebRpc {
ApiResult<String> get();
ApiResult<String> get2(String param);}
复制代码


public interface WebRpc2 {
ApiResult<String> get();
ApiResult<String> get(String param);}
复制代码


@Servicepublic class WebRpcImpl implements WebRpc {
@Overridepublic ApiResult<String> get() { return ApiResult.success("get success");}
@Overridepublic ApiResult<String> get2(String param) { return ApiResult.success(param); }}
复制代码


@Servicepublic class WebRpc2Impl implements WebRpc2 {
@Overridepublic ApiResult<String> get() { return ApiResult.success("get success");}
@Overridepublic ApiResult<String> get(String param) { return null; }}
复制代码


在 java-study-common-provider 包中有个 service 包,其中有两个 service 接口以及对应的实现类,分别是 CommonEntityService.class,CommonEntityService2.class,CommonEntityServiceImpl.class, CommonEntityService2Impl.class,在两个实现类中都有引用 rpc 接口。



public interface CommonEntityService {
ApiResult<Void> test(CommonEntity commonEntity);}
复制代码


public interface CommonEntityService2 {}
复制代码


@Servicepublic class CommonEntityServiceImpl implements CommonEntityService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowiredprivate CommonEntityManager commonEntityManager;@Autowiredprivate WebRpc webRpc;

@Overridepublic ApiResult<Void> test(CommonEntity commonEntity) { // webRpc 单元测试时可能为null ApiResult<String> getRpc = webRpc.get(); if (!getRpc.getSuccess()) { logger.info("getRpc fail: {}", getRpc); return ApiResult.error(getRpc); } ApiResult<String> getRpc2 = webRpc.get2("test"); if (!getRpc2.getSuccess()) { logger.info("getRpc2 fail: {}", getRpc2); return ApiResult.error(getRpc2); } // 依赖远程方法调用结果 Optional<String> remoteResultOpt = RmiUtil.getRemoteResult(); if (!remoteResultOpt.isPresent()) { logger.info("getRemoteResult fail"); return ApiResult.error(BizRespStatusEnum.SYS_ERR); } // 入库 int insertNo = commonEntityManager.insert(commonEntity); logger.info("insert {} common entity", insertNo); return ApiResult.success(null); }}
复制代码


@Servicepublic class CommonEntityService2Impl implements CommonEntityService2 {
@Autowiredprivate WebRpc2 webRpc2;}
复制代码


➮ 2-2.针对 CommonEntityService.class 编写单元测试


先加入 SpringBootTest 依赖。


<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions></dependency>
复制代码


创建对应的单元测试类。


@ExtendWith(SpringExtension.class)@SpringBootTest(classes = CommonTestApplication.class)public class CommonEntityServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowiredprivate CommonEntityService commonEntityService;
@Testpublic void test() { ApiResult<Void> testSuccess = commonEntityService.test(new CommonEntity()); Assert.isTrue(testSuccess.getSuccess(), "testSuccess fail"); logger.info("testSuccess: {}", JSON.toJSONString(testSuccess)); }}
复制代码


当我们去执行单元测试的 test() 方法时,会出现 NoSuchBeanDefinitionException 异常。


Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.peng.java.study.web.api.rpc.WebRpc2' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}        at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1695)        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1253)        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1207)        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)        ... 43 more
复制代码


这是因为我们执行单元测试的这个模块虽然依赖了 java-study-web-api 包,能够调用 rpc 方法,但是没有依赖 java-study-web-provider 包,没办法注入对应的实现类。


有三种方法可以解决这个问题:


I .将该单元测试类挪到 java-study-web-provider 包中,这样就能加载到所有的 bean 了。


这个方法有局限性,每次执行单元测试都需要加载所有模块的文件,大大的降低了单元测试的效率。


II .在注入 rpc 的注解 @Autowired 上加上 required = false


@Autowired(required = false)private WebRpc2 webRpc2;


这个方法有局限性,假设每次新增的 service 类都需要注入同一个 rpc 时,那每个 rpc 的注解 @Autowired 都需要使用 required = false,不然就没办法启动单元测试,由此可见是比较麻烦的。


III.使用 Mock,在执行单元测试前,将依赖但又没办法获取到实现类的 bean 注入进去。


将 mokito 包加入项目。

<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline --><dependency>    <groupId>org.mockito</groupId>    <artifactId>mockito-inline</artifactId>    <version>4.5.1</version>    <scope>test</scope></dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core --><dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.5.1</version> <scope>test</scope></dependency>
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy-agent --><dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.12.9</version> <scope>test</scope></dependency>
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy --><dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.12.8</version></dependency>
复制代码


使用 @MockBean 和 MockitoAnnotations.openMocks(this) 可以将依赖的 bean 注入进去。


@ExtendWith(SpringExtension.class)@SpringBootTest(classes = CommonTestApplication.class)public class CommonEntityServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowiredprivate CommonEntityService commonEntityService;@MockBeanpublic WebRpc webRpc;@MockBeanpublic WebRpc2 webRpc2;
@BeforeEachpublic void before(){ MockitoAnnotations.openMocks(this);}
@Testpublic void test() { ApiResult<Void> testSuccess = commonEntityService.test(new CommonEntity()); Assert.isTrue(testSuccess.getSuccess(), "testSuccess fail"); logger.info("testSuccess: {}", JSON.toJSONString(testSuccess)); }}
复制代码


此时再执行 test() 方法,不再出现 NoSuchBeanDefinitionException 异常,但会出现 NullPointerException 异常。这是因为我们虽然注入了 bean,但这个 bean 是个空的,因此在 commonEntityService.test 方法中执行 webRpc.get() 时,会报 NullPointerException 异常。


为解决这个问题,我们可以继续使用 mock,Mockito.when(). thenReturn()。



@ExtendWith(SpringExtension.class)@SpringBootTest(classes = CommonTestApplication.class)public class CommonEntityServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired private CommonEntityService commonEntityService; @MockBean public WebRpc webRpc; @MockBean public WebRpc2 webRpc2;
@BeforeEach public void before(){ MockitoAnnotations.openMocks(this); }
@Test public void test() { Mockito.when(webRpc.get()).thenReturn(ApiResult.success("mock result 1")); Mockito.when(webRpc.get2("test")).thenReturn(ApiResult.success("mock result 2")); ApiResult<Void> testSuccess = commonEntityService.test(new CommonEntity()); Assert.isTrue(testSuccess.getSuccess(), "testSuccess fail"); logger.info("testSuccess: {}", JSON.toJSONString(testSuccess)); }}
复制代码


再次执行 test() 方法,此时执行已经成功了,打印日志如下所示。

2022-05-21 22:23:23.094  INFO 3760 --- [           main] c.p.j.s.c.c.s.i.CommonEntityServiceImpl  : insert 0 common entity2022-05-21 22:23:23.161  INFO 3760 --- [           main] c.p.j.s.c.c.s.CommonEntityServiceTest    : apiResult: {"code":"200","msg":"调用成功","success":true}
复制代码


虽然已经成功执行了单元测试,但如果需要 mock 的 bean 很多的话,那不是每个测试类都需要写一遍 mock,很浪费时间啊,因此,我们可以把需要 mock 的 bean 全都放到一个类中进行管理。


@Componentpublic class CommonMockFactory {@BeforeEachpublic void before(){    MockitoAnnotations.openMocks(this);}
@MockBeanpublic WebRpc webRpc;@MockBeanpublic WebRpc2 webRpc2;
复制代码


然后在需要单元测试的类中进行注入即可。



@ExtendWith(SpringExtension.class)@SpringBootTest(classes = CommonTestApplication.class)public class CommonEntityServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired private CommonEntityService commonEntityService; @Autowired private CommonMockFactory commonMockFactory;
@Test public void test() { Mockito.when(commonMockFactory.webRpc.get()).thenReturn(ApiResult.success("mock result 1")); Mockito.when(commonMockFactory.webRpc.get2("test")).thenReturn(ApiResult.success("mock result 2")); ApiResult<Void> testSuccess = commonEntityService.test(new CommonEntity()); Assert.isTrue(testSuccess.getSuccess(), "testSuccess fail"); logger.info("testSuccess: {}", JSON.toJSONString(testSuccess)); }}
复制代码


➮ 2-3.提高单元测试覆盖率


使用 idea 自带的单元测试覆盖率工具可以查看相应的覆盖率。绿色的条代表已覆盖,红色的条代表未覆盖。


以下是单元测试的覆盖率文档,分别是类覆盖率、方法覆盖率、行覆盖率,从图中可以看出我们的行覆盖率只有 64%,还有提升的空间。


如何提升呢?答案就是 mock。

先上改造后的代码:



@ExtendWith(SpringExtension.class)@SpringBootTest(classes = CommonTestApplication.class)public class CommonEntityServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired private CommonEntityService commonEntityService; @Autowired private CommonMockFactory commonMockFactory;
@Test public void test() { Mockito.when(commonMockFactory.webRpc.get()).thenReturn(ApiResult.success("mock result 1")); Mockito.when(commonMockFactory.webRpc.get2("test")).thenReturn(ApiResult.success("mock result 2")); ApiResult<Void> testSuccess = commonEntityService.test(new CommonEntity()); Assert.isTrue(testSuccess.getSuccess(), "testSuccess fail"); logger.info("testSuccess: {}", JSON.toJSONString(testSuccess)); }
@Test public void testWithMock() { Mockito.when(commonMockFactory.webRpc.get()).thenReturn(ApiResult.success("mock result 1")); Mockito.when(commonMockFactory.webRpc.get2("test")).thenReturn(ApiResult.success("mock result 2")); ApiResult<Void> testSuccess = commonEntityService.test(new CommonEntity()); Assert.isTrue(testSuccess.getSuccess(), "testSuccess fail"); logger.info("testSuccess: {}", JSON.toJSONString(testSuccess));
// 模拟 webRpc.get() 失败 Mockito.when(commonMockFactory.webRpc.get()).thenReturn(ApiResult.error(BizRespStatusEnum.ILLEGAL_PARAM)); Mockito.when(commonMockFactory.webRpc.get2("test")).thenReturn(ApiResult.success("mock result 2")); ApiResult<Void> testFail1 = commonEntityService.test(new CommonEntity()); Assert.isTrue(!testFail1.getSuccess(), "testFail1 fail"); logger.info("testFail1: {}", JSON.toJSONString(testFail1));
Mockito.when(commonMockFactory.webRpc.get()).thenReturn(ApiResult.success("mock result 1")); // 模拟 webRpc.get2() 失败 Mockito.when(commonMockFactory.webRpc.get2("test")).thenReturn(ApiResult.error(BizRespStatusEnum.ILLEGAL_PARAM)); ApiResult<Void> testFail2 = commonEntityService.test(new CommonEntity()); Assert.isTrue(!testFail2.getSuccess(), "testFail1 fail"); logger.info("testFail2: {}", JSON.toJSONString(testFail2));
Mockito.when(commonMockFactory.webRpc.get()).thenReturn(ApiResult.success("mock result 1")); Mockito.when(commonMockFactory.webRpc.get2("test")).thenReturn(ApiResult.success("mock result 2")); try (MockedStatic<RmiUtil> rmiUtilMockedStatic = Mockito.mockStatic(RmiUtil.class)) { // 模拟 RmiUtil.getRemoteResult() 失败 rmiUtilMockedStatic.when(RmiUtil::getRemoteResult).thenReturn(Optional.empty()); ApiResult<Void> testFail3 = commonEntityService.test(new CommonEntity()); Assert.isTrue(!testFail3.getSuccess(), "testFail3 fail"); logger.info("testFail3: {}", JSON.toJSONString(testFail3)); } }}
复制代码


单元测试的执行结果。

2022-05-21 23:23:46.516  INFO 35136 --- [           main] c.p.j.s.c.c.s.i.CommonEntityServiceImpl  : insert 0 common entity2022-05-21 23:23:46.589  INFO 35136 --- [           main] c.p.j.s.c.c.s.CommonEntityServiceTest    : testSuccess: {"code":"200","msg":"调用成功","success":true}2022-05-21 23:23:46.590  INFO 35136 --- [           main] c.p.j.s.c.c.s.i.CommonEntityServiceImpl  : getRpc fail: ApiResult{success=false, code='400', msg='参数异常', result=null}2022-05-21 23:23:46.590  INFO 35136 --- [           main] c.p.j.s.c.c.s.CommonEntityServiceTest    : testFail1: {"code":"400","msg":"参数异常","success":false}2022-05-21 23:23:46.591  INFO 35136 --- [           main] c.p.j.s.c.c.s.i.CommonEntityServiceImpl  : getRpc2 fail: ApiResult{success=false, code='400', msg='参数异常', result=null}2022-05-21 23:23:46.591  INFO 35136 --- [           main] c.p.j.s.c.c.s.CommonEntityServiceTest    : testFail2: {"code":"400","msg":"参数异常","success":false}2022-05-21 23:23:46.629  INFO 35136 --- [           main] c.p.j.s.c.c.s.i.CommonEntityServiceImpl  : getRemoteResult fail2022-05-21 23:23:46.629  INFO 35136 --- [           main] c.p.j.s.c.c.s.CommonEntityServiceTest    : testFail3: {"code":"002","msg":"系统异常","success":false}
复制代码

再来看看改造之后的覆盖率!从下图中可以看出单元测试的行覆盖率达到了 100%,惊不惊喜,意不意外!




3、总结


在我们没用 mock 工具时,别说覆盖率了,执行一个单元测试都很麻烦。


使用 mock 工具之后,我们不仅可以很方便的执行单元测试,还能使用各种奇技淫巧来提升行覆盖率,强烈推荐!


写好单元测试一点都不简单,本文只是拿了一个简单的场景来举例,在单元测试的行覆盖率达到 100%时,代码量就已经是源码的两倍还多了,害!但是 bug 和单元测试总要选一个的,看大家的选择了,哈哈哈。


了解更多敏捷开发、项目管理、行业动态等消息,可关注LigaAI,或点击浏览我们的官网LigaAI-新一代智能研发管理平台 获取更多咨讯,LigaAI 期待与你一路同行,助力开发者扬帆远航!

用户头像

LigaAI

关注

新一代智能研发协作平台 2021.02.23 加入

AI赋能工作场景,想要做最懂开发者的智能研发管理平台~

评论

发布
暂无评论
技术分享 | Javaer 如何做单元测试?_Java_LigaAI_InfoQ写作社区