基于SpringBoot聊单元测试的分层

2022-09-20 15:56:00 浏览数 (1)

之前分享了关于质量内建的话题关于单元测试引起了大家的讨论,对于单元测试这件事情本身是比较熟悉的,但大家的反馈是比较难执行,矛盾在于很多测试做不了单元测试,或者让测试做性价比不是很高,这件事情推给开发之后又容易不了了之,其中一个很重要的点是,测试和开发没有同频对话的能力,各种细节难以敲定,落地的实际价值不容易度量,所以这篇文章我就基于常见的springboot框架,聊一聊单元测试分层的几种实践方式,从测试的视角给同学们一些知识面的拓展,也让大家熟悉下单元测试的常见玩法。

一.单元测试带来的好处

1.预防bug

为什么说可以预防bug呢,如果能够执行单元测试,说明开发已经具备一定的质量思维了,在写代码的时候会考虑如何测试,有哪些测试点等,通过这样的思维可以预防bug的产生。

2.快速定位Bug

单元测试意味着我们测试的前置以及测试颗粒度的细化,所以更容易在更小范围内锁定bug,能够带来效率的提升,相对于在测试阶段发现bug来说,会大量减少调试时间。

3.降低重构风险

快速的发现并解决问题不容易形成技术债,团队具备良好的质量把控意识会从根本上带来质量的提升,从而降低重构的可能性。

二.SpringBoot的测试库

SpringBoot提供了如下的类库,通过引入可以获取到测试的类方法。

代码语言:javascript复制
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

Junit:Java应用程序单元测试标准类库

AssertJ:轻量级断言类库

Mockito: Java的Mock测试框架

JsonPath:JSON操作类库

JSONNAssert:基于JSON的断言库

三.快速创建单元测试

当我们引入spring-boot-starter-test相关的类库后,直接在工程项目中src/test/java中创建类即可,如下所示:

代码语言:javascript复制
package com.example.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UnitTestDemoApplicationTests {
   @Test
   public void contextLoads() {
   }
}

@SpringBootTest:是SpringBoot用于测试的注解,可指定入口类和测试环境。

@RunWith(SpringRunner.class):让测试运行于Spring的测试环境。

@Test 表示为一个测试单元。

四:SpingBoot基础知识

先来简单看下我们如何访问springboot服务,当用户通过浏览器访问后端服务时,通过Controller层决定控制访问逻辑,Service层主要实现系统的业务逻辑,DAO层直接操作数据库的代码。

总结这三者,通过例子来解释:

Controller像是服务员,顾客点什么菜,菜上给几号桌,都是ta的职责;

Service是厨师,action送来的菜单上的菜全是ta做的;

Dao是厨房的小工,和原材料打交道的事情全是ta管。

五.单元测试的分层实践

1.基于Controller层的单元测试

关于实践就直接通过代码演示,首先可以在controller层实现一下demo,在src/test/java下完成

代码语言:javascript复制
package com.example.demo.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello(String name){
        return "hello " name;
    }
   }

解释下:

@RestController:代表这个类是REST风格的控制器,返回JSON或者XML的类型数据。

@RequestMapping:用于配置URL和方法之间的映射,可用在类和方法上。

编写测试代码:

代码语言:javascript复制
package com.example.demo.controller;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.Assert.*;

@SpringBootTest
@RunWith(SpringRunner.class)
public class HelloControllerTest {
    //启用web上下文
    @Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception{
        //使用上下文构建mockMvc
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }
    @Test
    public void hello() throws Exception {
       // 得到MvcResult自定义验证
      // 执行请求
        MvcResult mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/hello")
        //.post("/hello") 发送post请求
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                //传入参数
                .param("name","cctester")
               // .accept(MediaType.TEXT_HTML_VALUE))
                //接收的类型
                .accept(MediaType.APPLICATION_JSON_UTF8))
                //等同于Assert.assertEquals(200,status);
                //判断接收到的状态是否是200
                .andExpect(MockMvcResultMatchers.status().isOk())
                 //等同于 Assert.assertEquals("hello cctetser",content);
                .andExpect(MockMvcResultMatchers.content().string("hello cctester"))
                .andDo(MockMvcResultHandlers.print())
        //返回MvcResult
        .andReturn();
        //得到返回代码
        int status=mvcResult.getResponse().getStatus();
        //得到返回结果
        String content=mvcResult.getResponse().getContentAsString();
        //断言,判断返回代码是否正确
        Assert.assertEquals(200,status);
        //断言,判断返回的值是否正确
        Assert.assertEquals("hello cctester",content);
    }
}

通过相应的输出可以完成校验,可以看到controller级别的单元测试跟接口测试比较接近

2.Service层的单元测试

我们还是先来创建demo,先来一个实体类

代码语言:javascript复制
package com.example.demo.entity;
import lombok.Data;
@Data
public class User {
    private String name;
    private  int age;
}

再创建服务类,@Service来标注服务类

代码语言:javascript复制
package com.example.demo.service;
import com.example.demo.entity.User;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User getUserInfo(){
        User user = new User();
        user.setName("cctester");
        user.setAge(28);
        return user;
    }
}

编写测试用例

代码语言:javascript复制
package com.example.demo.service;
import com.example.demo.entity.User;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.hamcrest.CoreMatchers.*;
//表明要在测试环境运行,底层使用的junit测试工具
@RunWith(SpringRunner.class)
// SpringJUnit支持,由此引入Spring-Test框架支持!
//启动整个spring的工程
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
    @Test
    public void getUserInfo() {
        User user = userService.getUserInfo();
        //比较实际的值和用户预期的值是否一样
        Assert.assertEquals(18,user.getAge());
        Assert.assertThat(user.getName(),is("cctester"));
    }
}

运行结果:

3.DAO层的单元测试

DAO层主要用于对数据的增删改查操作,同样可以进行单元测试,并使用@Transactional注解进行回滚操作,我们也来简单演示下

代码语言:javascript复制
package com.example.demo.repository;

import com.example.demo.entity.Card;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
// SpringJUnit支持,由此引入Spring-Test框架支持!
//启动整个spring的工程
@SpringBootTest
//@DataJpaTest
@Transactional
//@Rollback(false)
public class CardRepositoryTest {
    @Autowired
    private  CardRepository  cardRepository;
    @Test
  public void testQuery() {
   // 查询操作
  List<Card> list = cardRepository.findAll();
        for (Card card : list) {
            System.out.println(card);
        }
   }
    @Test
    public void testRollBank() {
        // 写入操作
        Card card=new Card();
        card.setNum(3);
        cardRepository.save(card);
        //throw new RuntimeException();
    }

运行testRollBack,可以看到输出台

代码语言:javascript复制
hibernate: insert into cardtestjpa (num) values (?)
2022-04-17 09:21:25.866  INFO 27907 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@3bb9a3ff testClass = CardRepositoryTest, testInstance = com.example.demo.repository.CardRepositoryTest@2bf3ec4, testMethod = testRollB

0 人点赞