기술 공유 | Javaer는 단위 테스트를 어떻게 수행합니까?

머리말:

이 문서는 javaer에 적용되며 다른 개발자도 이를 통해 배울 수 있습니다.

이 기사를 작성하는 데는 두 가지 주요 주제가 있습니다. 하나는 단위 테스트를 간략하게 소개하는 것이고 다른 하나는 모든 사람이 단위 테스트를 작성할 수 있는 임계값을 낮추기를 바라는 간단한 예제를 통해 몇 가지 단위 테스트 기술을 소개하는 것입니다.

1. 단위 테스트의 정의


단위 테스트는 일반적으로 응용 프로그램의 일부("단위"라고 함)가 예상대로 설계 및 기능을 준수하는지 확인하기 위해 소프트웨어 개발자가 작성하고 실행하는 자동화된 테스트입니다. 프로그래밍하는 동안 단위는 완전한 모듈이 될 수 있지만 더 일반적으로 단일 함수 또는 프로시저 개체 지향 프로그래밍에서 단위는 일반적으로 클래스 또는 단일 메서드와 같은 완전한 인터페이스입니다. 먼저 테스트 가능한 가장 작은 단위에 대한 테스트를 작성한 다음 이들 간의 복합 동작은 포괄적인 테스트를 빌드할 수 있습니다. 복잡한 응용 프로그램을 위해.

단위 테스트는 일반적으로 소프트웨어 개발자가 작성하고 실행하는 자동화된 테스트로 애플리케이션의 섹션("단위"로 알려짐)이 설계를 충족하고 의도한 대로 동작하는지 확인합니다. 절차적 프로그래밍에서 단위는 전체 모듈일 수 있지만 일반적으로 개별 기능이나 절차입니다. 객체 지향 프로그래밍에서 단위는 종종 클래스 또는 개별 메서드와 같은 전체 인터페이스입니다. 테스트 가능한 가장 작은 단위에 대한 테스트를 먼저 작성한 다음 이들 간의 복합 동작을 작성하여 복잡한 애플리케이션에 대한 포괄적인 테스트를 구축할 수 있습니다. —위키피디아,단위 테스트

간단히 말해서, 단위 테스트는 단위에 대한 테스트 방법을 작성하는 것입니다. 이 장치는 매우 단순한 기능일 수도 있고 다양한 다른 기능의 호출을 포함할 수 있는 완전한 인터페이스일 수도 있습니다.

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. 프로젝트 파일 준비

다음과 같은 프로젝트 파일에 대한 디렉토리 구조가 있다고 가정합니다.

798.png

종속성은 다음과 같습니다.

java-study-web-provider 依赖 java-study-web-api, java-study-common-provider
java-study-web-api 依赖 java-study-common-api
java-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 包中。

677.png


public interface WebRpc {

    ApiResult<String> get();

    ApiResult<String> get2(String param);
}
复制代码

public interface WebRpc2 {

    ApiResult<String> get();

    ApiResult<String> get(String param);
}
复制代码
@Service
public class WebRpcImpl implements WebRpc {

    @Override
    public ApiResult<String> get() {
        return ApiResult.success("get success");
    }

    @Override
    public ApiResult<String> get2(String param) {
        return ApiResult.success(param);
    }
}
复制代码
@Service
public class WebRpc2Impl implements WebRpc2 {

    @Override
    public ApiResult<String> get() {
        return ApiResult.success("get success");
    }

    @Override
    public ApiResult<String> get(String param) {
        return null;
    }
}
复制代码

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

56.png


public interface CommonEntityService {

    ApiResult<Void> test(CommonEntity commonEntity);
}
复制代码

public interface CommonEntityService2 {
}
复制代码
@Service
public class CommonEntityServiceImpl implements CommonEntityService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private CommonEntityManager commonEntityManager;
    @Autowired
    private WebRpc webRpc;


    @Override
    public 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);
    }
}
复制代码

@Service
public class CommonEntityService2Impl implements CommonEntityService2 {

    @Autowired
    private 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());

    @Autowired
    private CommonEntityService commonEntityService;

    @Test
    public 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());

    @Autowired
    private CommonEntityService commonEntityService;
    @MockBean
    public WebRpc webRpc;
    @MockBean
    public WebRpc2 webRpc2;

    @BeforeEach
    public void before(){
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public 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 entity
2022-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 全都放到一个类中进行管理。

@Component
public class CommonMockFactory {

    @BeforeEach
    public void before(){
        MockitoAnnotations.openMocks(this);
    }

    @MockBean
    public WebRpc webRpc;
    @MockBean
    public 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自带的单元测试覆盖率工具可以查看相应的覆盖率。绿色的条代表已覆盖,红色的条代表未覆盖。

45234.png

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

564.png

如何提升呢?答案就是 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 entity
2022-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 fail
2022-05-21 23:23:46.629  INFO 35136 --- [           main] c.p.j.s.c.c.s.CommonEntityServiceTest    : testFail3: {"code":"002","msg":"系统异常","success":false}
复制代码

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

43.png 34.png

3、总结


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

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

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

➮ 想要了解更多程序人生、敏捷开发、项目管理、行业动态等消息,欢迎关注Liga@juejin了解更多详情,或点击我们的官方网站 LigaAI-智能研发协作平台线上申请体验。

рекомендация

отjuejin.im/post/7102329483935350815
рекомендация