如何进行单元测试

大话单测

代码写完了,是不是就万事大吉,只差上线了?我不知道大家有没有写单测的习惯,有的公司要求写,有的公司根本就不要求写。

写单元测试确实是一个体力活,一点都不比写代码轻松,所以很多程序员比较排斥单元测试,更不用说集成测试了。

如果不写单元测试,那代码的质量如何保证呢,咱不是有QA吗,靠QA呗,出了事儿算QA的,但研发一定是幸免于难的,必定这个事儿是你干的,至少有你一半的责任。

单元测试是质量的第一道把关,至关重要.

那什么样的单元测试是好的呢,干一个事儿肯定得有衡量的方法,我给你一一介绍。

环境搭建

单元测试还需要搭建环境吗?我们的脚手架不都已经搭建好了吗?如何搭建Spring Boot脚手架 - 掘金 (juejin.cn)但我想给你介绍一下另外一种好玩的方式。

在目前分布式的体系架构下,我们是不是要依赖很多中间件,比如Redis、DB、Kafka等,如果做单元测试的话,依赖的中间件怎么办呢?

使用dev环境的中间件不就可以了吗?但会遇到一个问题是,dev环境大家共用,万一不小心破坏了你的数据,不就game over了吗?总不能不让别人用吧。

除了环境的问题,还需要考虑每个case之间的数据是隔离的,这样才能保证case的准确性。

什么叫准确性呢,这个case执行1次和执行100次的结果是一样的,不会因着其他因素而改变,这也是科学中的可重复性。

在介绍脚手架的文章中,我们提到了内存版的Redis、kafka、Db, 接下来我们来看看怎么使用的,直接上代码

导入pom

  1. <dependency>

  2. <groupId>it.ozimov</groupId>

  3. <artifactId>embedded-redis</artifactId>

  4. <version>0.7.2</version>

  5. <scope>test</scope>

  6. </dependency>

  7. <dependency>

  8. <groupId>ch.vorburger.mariaDB4j</groupId>

  9. <artifactId>mariaDB4j</artifactId>

  10. <version>2.4.0</version>

  11. <scope>test</scope>

  12. </dependency>

  13. <dependency>

  14. <groupId>org.mariadb.jdbc</groupId>

  15. <artifactId>mariadb-java-client</artifactId>

  16. <version>2.5.2</version>

  17. <scope>test</scope>

  18. </dependency>

  19. <dependency>

  20. <artifactId>junit-platform-launcher</artifactId>

  21. <groupId>org.junit.platform</groupId>

  22. <scope>test</scope>

  23. </dependency>

  24. <dependency>

  25. <artifactId>junit-vintage-engine</artifactId>

  26. <groupId>org.junit.vintage</groupId>

  27. <version>5.9.0</verison>

  28. </dependency>

ApplicationTests 测试启动类:该类主要职责是启动redis Server,DbServer.

  1. @SpringBootTest(classes = {LifeCycleManagement.class, KafkaTemplateConfig.class})

  2. @ActiveProfiles({"unit"})

  3. @Slf4j

  4. @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)

  5. public abstract class ApplicationTests {

  6. public static RedisServer redisServer;

  7. @BeforeAll

  8. public static void beforeAll() throws ManagedProcessException {

  9. log.info("================mariadb start================");

  10. LifeCycleManagement.initDB();

  11. log.info("================redis server start================");

  12. redisServer = RedisServer.builder().setting("maxmemory 200m").port(8697).build();

  13. redisServer.start();

  14. }

  15. @AfterAll

  16. public static void afterAll() throws ManagedProcessException {

  17. log.info("================mariadb close================");

  18. LifeCycleManagement.closeDB();

  19. log.info("================redis server stop================");

  20. redisServer.stop();

  21. }

  22. }

LifeCycleManagement 该类职责是初始化DB库,启动kafka Server

  1. @TestConfiguration

  2. @Slf4j

  3. public class LifeCycleManagement {

  4. private static final int NUMBER_OF_BROKERS = 1;

  5. public static DB db;

  6. @Value("${spring.cloud.sentinel.enabled}")

  7. private static boolean sentinel;

  8. public static void initDB() throws ManagedProcessException {

  9. DBConfigurationBuilder configBuilder = DBConfigurationBuilder.newBuilder();

  10. configBuilder.setPort(3308); // OR, default: setPort(0); => autom. detect free port

  11. configBuilder.setDataDir("./data"); // just an example

  12. configBuilder.addArg(" --user=root");

  13. db = DB.newEmbeddedDB(configBuilder.build());

  14. db.start();

  15. db.createDB("test");

  16. db.source("script/source_table_str.sql", "test");

  17. }

  18. public static void closeDB() throws ManagedProcessException {

  19. if (db != null) {

  20. db.stop();

  21. }

  22. }

  23. public static int[] setupPorts() {

  24. return new int[NUMBER_OF_BROKERS];

  25. }

  26. @Bean

  27. public EmbeddedKafkaBroker initKafka() {

  28. log.info("================kafka server start================");

  29. boolean CONTROLLER_SHUTDOWN = true;

  30. int NUMBER_OF_PARTITIONS = 1;

  31. EmbeddedKafkaBroker embeddedKafkaBroker =

  32. new EmbeddedKafkaBroker(NUMBER_OF_BROKERS, CONTROLLER_SHUTDOWN, NUMBER_OF_PARTITIONS,

  33. new String[]{})

  34. .kafkaPorts(setupPorts()).zkPort(0)

  35. .zkConnectionTimeout(EmbeddedKafkaBroker.DEFAULT_ZK_CONNECTION_TIMEOUT)

  36. .zkSessionTimeout(EmbeddedKafkaBroker.DEFAULT_ZK_SESSION_TIMEOUT);

  37. Properties properties = new Properties();

  38. properties.put("listeners", "PLAINTEXT://127.0.0.1:9091");

  39. properties.put("port", "9091");

  40. properties.put("auto.create.topics.enable", true);

  41. embeddedKafkaBroker.brokerProperties((Map<String, String>) (Map<?, ?>) properties);

  42. return embeddedKafkaBroker;

  43. }

  44. }

BaseTest 该类主要初始化mockMvc

  1. public abstract class BaseTest extends ApplicationTests {

  2. public static MockMvc mockMvc;

  3. @Autowired

  4. WebApplicationContext webApplicationContext;

  5. @AfterEach

  6. void afterEach() {

  7. }

  8. @BeforeEach

  9. public void beforeEach() throws ManagedProcessException {

  10. MiddleWareLifeCycleManagement.db.source("script/clean.sql", "test");

  11. // 在这初始mock所有的过滤器都不会加载

  12. mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)

  13. .addFilter(webApplicationContext.getBean(ContentCachingTestFilter.class)).build();

  14. }

  15. protected ResultActions doAction(String content, String uri) throws Exception {

  16. return mockMvc

  17. .perform(MockMvcRequestBuilders.post(uri).contentType(MediaType.APPLICATION_JSON)

  18. .header("requestTime", System.nanoTime()).content(content));

  19. }

  20. protected ResultActions doAction(MultiValueMap<String, String> map, String uri)

  21. throws Exception {

  22. return mockMvc.perform(MockMvcRequestBuilders.post(uri).params(map).header("bizId",

  23. UUID.randomUUID().toString().replace("-", "")));

  24. }

  25. }

在你的工程中导入以下几个类,基本上就可以work了

单元测试如何写

直接看案例

  1. @Test

  2. void should_return_expired_when_status_isCorrect() throws Exception {

  3. ReceiveAwardRequest request = givenStatusExpired(); //given

  4. assertResponseCodeEquals(request,ResultCode.EXPIRED); //when and then

  5. }

单元测试基本上遵循这样的结构体

given : 封装请求参数

when:执行请求

then:assert断言验证

方法名如何进行命名呢?这两种方式我在项目中都用了,但我更喜欢第二种方式。

  1. givenXXX_whenXXX_thenXXX, 从方法命名上一目了然
  2. should_XXX_when_XXX

说完了结构,接下来我给大家介绍一下不同类型的单元测试该如何写

1. 断言http接口返回值

spring的MockMvc可以帮我发起http请求

get请求

  1. mockMvc

  2. .perform(MockMvcRequestBuilders.get(uri).queryParams(map));

post请求

  1. mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/receive")

  2. .contentType(MediaType.APPLICATION_JSON)

  3. .header("requestTime", System.nanoTime())

  4. .content(content)) .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(resultCode.getCode()));

如果你想mock第三方服务的返回值,可以这么做

Mockito.doReturn(false).when(xxx).xxxFunction(Mockito.any());

有时候我们需要mock spring中的bean,该bean仍然需要spring去管理,而不是mock,可以这样做

  1. @SpyBean

  2. protected xxxService xxservice

  3. 或者

  4. Mockito.spy(bean)

2. 断言数据库中的某个值

在有些情况下,我们验证该case的是否成功的条件是验证数据库中的某个值是不是改变了,比如验证一下奖券状态修改从a->b,判断该case是否成功就需要看数据库的值是否是b

  1. Awaitility.await().pollDelay(100, TimeUnit.MILLISECONDS)

  2. .pollInterval(Duration.ofMillis(500))

  3. .until(() -> eventService.lambdaQuery()

  4. .eq(MsgEvent::getRequestId, 112212)

  5. .eq(MsgEvent::getState, MsgEventState.FAILED.getCode()).exists());

3. 断言某个方法是否被执行到

Mockito.verify(xxxx, Mockito.never()).xxxFunction(Mockito.any());

4. 直接mock某个对象

Mockito.mock(Clazz class)

除此之外还可以断言某个日志关键字是否被打印等等

如何执行单元测试

执行一个单元测试,我们都知道,直接在单个case上右键点击run就行,如果要执行整个项目的单元测试该如何做呢?

配置完成之后,直接点击run。

执行单元测试必须要依赖于IDE吗,当然不是了,通过命令的方式也可以。

单测覆盖率

单元测试写完了,如何衡量单测写的好不好呢?单测覆盖率,我们一般使用分支覆盖率来衡量

什么是分支覆盖率呢?你理解成对if else的覆盖情况,如果只覆盖到了if,那覆盖率就是50%

单测覆盖率如何执行呢?

1、在pom.xml中添加jacoco插件

  1. <plugin>

  2. <groupId>org.jacoco</groupId>

  3. <artifactId>jacoco-maven-plugin</artifactId>

  4. <version>0.8.6</version>

  5. <executions>

  6. <execution>

  7. <goals>

  8. <goal>prepare-agent</goal>

  9. </goals>

  10. </execution>

  11. <execution>

  12. <id>report</id>

  13. <phase>test</phase>

  14. <goals>

  15. <goal>report</goal>

  16. </goals>

  17. </execution>

  18. </executions>

  19. </plugin>

2、执行命令

mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent test surefire:test surefire-report:report -Dsurefire.reportsDirectory=./target/site --settings=D:/IdeaProjects/settings.xml

3、结果

每个包的分支覆盖率,指令覆盖率都赫然显示出来,点击包就能看到某个类的覆盖率以及代码执行情况

总结

单元测试需要花很长时间写,导致很多程序员不想写也不愿意写,但单元测试的收益是非常大的,在我们下次修改代码的时候,通过执行单元测试对软件进行第一道把关。除此之外,我们做重构也会比较放心,真的是 write once ,run any time。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值