前言:
本文实用于 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-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 包中。
public interface WebRpc {ApiResult<String> get();
ApiResult<String> get2(String param);
}
public interface WebRpc2 {ApiResult<String> get();
ApiResult<String> get(String param);
}
在 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 {}
@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 自带的单元测试覆盖率工具能够查看相应的覆盖率。绿色的条代表已笼罩,红色的条代表未笼罩。
以下是单元测试的覆盖率文档,别离是类覆盖率、办法覆盖率、行覆盖率,从图中能够看出咱们的行覆盖率只有 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 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%,惊不惊喜,意不意外!
3、总结
在咱们没用 mock 工具时,别说覆盖率了,执行一个单元测试都很麻烦。
应用 mock 工具之后,咱们不仅能够很不便的执行单元测试,还能应用各种奇技淫巧来晋升行覆盖率,强烈推荐!
写好单元测试一点都不简略,本文只是拿了一个简略的场景来举例,在单元测试的行覆盖率达到 100% 时,代码量就曾经是源码的两倍还多了,害!然而 bug 和 单元测试 总要选一个的,看大家的抉择了,哈哈哈。
理解更多麻利开发、项目管理、行业动态等音讯,关注咱们的 sf 账号 -LigaAI~ 或者点击 LigaAI- 新一代智能研发合作平台,,在线申请体验咱们的产品。