Compartilhamento de tecnologia | Como o Javaer faz testes de unidade?

Prefácio:

Este artigo se aplica ao javaer e outros desenvolvedores podem aprender com ele.

Existem dois temas principais para escrever este artigo, um é apresentar brevemente o teste de unidade para você e o outro é apresentar algumas habilidades de teste de unidade por meio de um exemplo simples, esperando diminuir o limite para que todos escrevam testes de unidade.

1. Definição de teste de unidade


Testes de unidade geralmente são testes automatizados escritos e executados por desenvolvedores de software para garantir que uma parte de um aplicativo (chamada de "unidade") esteja em conformidade com seu design e funções conforme o esperado. Durante a programação, uma unidade pode ser um módulo completo, mas mais comumente uma única função ou procedimento. Na programação orientada a objetos, uma unidade geralmente é uma interface completa, como uma classe ou um único método. Ao escrever primeiro testes para a menor unidade testável, depois para eles O comportamento composto entre elas pode construir testes abrangentes para aplicações complexas.

Os testes de unidade são normalmente testes automatizados escritos e executados por desenvolvedores de software para garantir que uma seção de um aplicativo (conhecida como “unidade”) atenda ao seu design e se comporte como pretendido. Na programação procedural, uma unidade pode ser um módulo inteiro, mas é mais comumente uma função ou procedimento individual. Na programação orientada a objetos, uma unidade geralmente é uma interface inteira, como uma classe, ou um método individual. Ao escrever testes primeiro para as menores unidades testáveis, depois os comportamentos compostos entre elas, pode-se construir testes abrangentes para aplicações complexas. —Wikipédia,Teste unitário

Simplificando, o teste de unidade é escrever um método de teste para uma unidade. A unidade pode ser uma função muito simples, ou pode ser uma interface completa, que pode conter chamadas de várias outras funções.

2. Casos de teste de unidade


A versão SpringBoot para este projeto é 2.2.5.RELEASE.

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

2-1. Preparação do arquivo do projeto

Suponha que haja uma estrutura de diretórios para arquivos de projeto da seguinte forma:

798.png

As dependências são:

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-智能研发协作平台线上申请体验。

Acho que você gosta

Origin juejin.im/post/7102329483935350815
Recomendado
Clasificación