Springboot 单元测试详解

1.Springboot的测试类库

SpringBoot 提供了许多实用工具和注解来帮助测试应用程序,主要包括以下两个模块。

  • spring-boot-test: 支持测试的核心内容。
  • spring-boot-test-autoconfigure:支持测试的自动化配置。

开发进行只要引入spring-boot-starter-test的依赖 就能引入这些SpringBoot测试模块,还能引入一些像Junit,AssertJ,Hamcrest及其他一些有用的类库,具体如下所示。

  • Junit: Java应用程序单元测试标准类库。
  • Spring Test & Spring Boot Test: Spring Boot 应用程序功能集成化测试支持。
  • AssertJ: 一个轻量级断言类库。
  • Hamcrest: 一个对象匹配器类库。
  • Mockito: 一个java Mock测试框架。
  • JSONassert: 一个用于JSON的断言库。
  • JsonPath: 一个Json操作类库。

Maven 依赖

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

2.创建测试类和测试方法

要让一个普通类变成一个单元测试类:

  • 1.在雷鸣上加入@SpringBootTest 和@RunWith(SpringRunner.class)两个注解即可。
  • 2.在测试方法上加上@Test注解。

使用IDEA可以使用快捷键ctrl + shift + t 或者选中当前类名 使用快捷键alt + enter ,向下选择Create Test 即可进入测试类的选项中,再次回车,就快速的生成测试类。

Springboot 单元测试详解_第1张图片
快速生成测试类界面

生成的测试类在src/test目录下,测试类和源代码宝明是一致的。

Springboot 单元测试详解_第2张图片
生成后的测试类

3.JUnit4

JUnit4中的注解

  • @BeforeClass:针对所有测试,只执行一次,且必须为static void
  • @Before:初始化方法,执行当前测试类的每个测试方法后执行
  • @Test:测试方法,在这里可以测试期望异常和超时时间
  • @After:释放资源,执行当前测试类的每个测试方法后执行
  • @AfterClass:针对所有测试,只执行一次,且必须为static void
  • Ignore :忽略的测试方法
  • @Runwith:可以更改测试运行器,缺省值org.junit.runner.Runner

一个单元测试类执行顺序为:
@BeforeClass -> @Before -> @Test -> @After -> @AfterClass

每一个测试方法的调用顺序为:
@Before -> @Test -> @After

3.1超时测试

如果一个测试用例比起指定的毫秒数要花费更多时间,那么JUnit将自动将他标记为失败,timeout参数和@test注解一起使用。现在让我们看看活动中的@Test(timeout)

@Test(timeout = 1000)
public void testTimeout() throws InterruptedException {
    TimeUnit.SECONDS.sleep(2);
    System.out.println("Complete");
}

上面测试会失败,在一秒后会抛出异常org.junit.runners.model.TestTimeOutException:test timedout after 1000 millseconds

3.2异常测试

你可以测试代码是否它抛出想要得到的异常。expected参数和@Test 注释一起使用。现在让我们看看活动中的@Test(expected)

    @Test(expected = NullPointerException.class)
    public void testNullException() {
        throw new NullPointerException();
    }

3.3套件测试

public class TaskOneTest {
    @Test
    public void test(){
        System.out.println("task one do");
    }
}

public class TaskTwoTest {
    @Test
    public void test(){
        System.out.println("task two do");
    }
}

public class TaskThreeTest {
    @Test
    public void test(){
        System.out.println("task three do");
    }
}

/*1. 更改测试运行方式为 Suite*/
@RunWith(Suite.class)
/*2. 将测试类传入进来*/
@Suite.SuiteClasses({TaskOneTest.class,TaskTwoTest.class,TaskThreeTest.class})
public class SuitTest {
    /*测试套件的入口类知识组织测试类一起进行测试,无任何测试方法*/
}

3.4参数化测试

JUnit4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试。
创建参数化测试,遵循以下5个步骤。

  • RunWith(Parameterized.class)来注释test类。
  • 创建一个由@Parameters注释的公共的静态方法,它返回一个对象的集合(数组)来作为数据集合。
  • 创建一个公共的构造函数,它接受数据集合相同的参数。
  • 为每一列测试数据创建一个实例变量。
  • 用实例变量作为测试数据的来源来创建你的测试用例。
/*更改默认的测试运行器为RunWith(Parameterized.class)*/
@RunWith(Parameterized.class)
public class ParameterTest {

    /*声明变量存放预期值和测试数据*/
    private String firstName;
    private String lastName;

    /*声明一个返回值 为Collection的公共静态方法,并使用@Parameters进行修饰*/
    @Parameterized.Parameters
    public static List param() {
        /*这里给出两个测试用例*/
        return Arrays.asList(new Object[][]{{"Mike","Black"},{"Circle","Smith"}});
    }

    public ParameterTest (String firstName,String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    /*进行测试,发现它会将所有的测试用例测试一遍*/
    @Test
    public void test(){
        String name = firstName + " " + lastName;
        System.out.println(name);
    }


4.Assert

assert常用方法

  • assertEquals("message",A,B):判断对象A和B是否相等,这个判断比较时调用了equals()方法。
  • assertSame("message",A,B):判断对象A和B是否相同,使用的是==操作符。
  • assertTure("message",A):判断A条件是否为真。
  • assertFalse("message",A):判断A条件是否不为真。
  • assertNotNull("message",A):判断A对象是否不为null
  • assertArrayEquals("message",A,B): 判断A数组与B数组是否相等。

5.Mockito

什么是mock
在面向对象的程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。

为什么使用Mock对象
使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。

在以下情况可以采用模拟对象来替代真实对象:

  • 真实对象的行为是不确定的(例如,当前的时间或温度)。
  • 真实对象很难搭建起来。
  • 真实对象的行为很难触发(例如,网络错误)。
  • 真实对象速度很难。
  • 真实对象是用户界面,或包括用户界面在内。
  • 真实的对象使用了回调机制。
  • 真实对象可能还不存在。
  • 真实对象可能包含不能用作测试的信息和方法。

使用Mockito一般分为三个步骤:

  • 1.模拟测试类所需的外部依赖
  • 2.执行测试代码
  • 3.判断执行结果是否达到预期

Mockito
JUnit和SpringTest基本上可以满足绝大多数单元测试,但是由于现在系统越来越复杂,相互之间依赖越来越多。特别是微服务化以后的系统,往往一个模块的代码需要依赖几个其他模块的东西。因此,在做单元测试的时候,往往很难构造出需要的依赖。一个单元测试,我们只关心一个小的功能,但是为了这个小的功能能跑起来,可能需要依赖一堆其他的东西,这就导致了单元测试无法进行。所以我们就需要在测试过程中引入mock测试。

所谓的Mock测试就是在测试过程中,对于一些不容易构造的、或者和这次单元测试无关但是上下文又有依赖的对象,用一个虚拟的对象(Mock对象)来模拟,以便单元测试能够进行。
比如有一段代码的依赖为:

Springboot 单元测试详解_第3张图片
代码依赖

当我们要进行单元测试的时候,就需要给A注入BC但是C又依赖了DD又依赖了E。这就导致了A的单元测试很难进行。

但是当我们使用Mock来进行模拟对象后,我们就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockBMockC指定一个明确的行为。就像这样:

Springboot 单元测试详解_第4张图片
Mock对象示意图

因此,当我们使用Mock后,对于那些难以构建的对象,就变成了个模拟对象,只需要提前的做Stubbing(桩)即可。所谓的做桩数据,也就是告诉Mock对象,当与之交互时执行何种行为过程。比如当调用B对象的b()方法时,我们期望返回一个true,这就是一个设置桩数据的预期。

mockito 使用详解

现有如下代码:
实体类

@Entity
@Data
@NoArgsConstructor
public class User implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true,nullable = false,length = 50)
    private String username;

    private String password;

    @CreationTimestamp
    private Date createDate;

    public User(Long id,String username) {
        this.id = id;
        this.username = username;
    }
}

Repository

public interface IUserRepository extends JpaRepository{
    boolean updateUser(User user);
}

Service

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements IUserService {
    private final IUserRepository userRepository;


    @Override
    public User findOne(Long id) {
        return userRepository.getOne(id);
    }

    @Override
    public boolean updateUsername(Long id, String username) {
        User user = findOne(id);
        if(user == null) {
            return false;
        }
        user.setUsername(username);
        return userRepository.updateUser(user);
    }
}

Test

public class IUserServiceTest {
    private IUserService userService;

//    @Mock
    private IUserRepository userRepository;


    @Before
    public void setUp() throws Exception {
        /*对所有注解了@Mock的对象进行模拟*/
//        MockitoAnnotations.initMocks(this);
        /*如果不使用注解,可以对单个对象进行mock*/
        userRepository = Mockito.mock(IUserRepository.class);
        /*构造测试对象*/
        userService = new UserServiceImpl(userRepository);
        /*打桩,构建当userRepository getOne函数执行参数为1的时候,设置返回的结果User*/
        Mockito.when(userRepository.getOne(1L)).thenReturn(new User(1L,"jack"));
         /*打桩,构建当userRepository getOne函数执行参数为1的时候,设置返回的结果null*/
        Mockito.when(userRepository.getOne(2L)).thenReturn(null);
         /*打桩,构建当userRepository getOne函数执行参数为1的时候,设置抛出异常*/
        Mockito.when(userRepository.getOne(3L)).thenThrow(new IllegalArgumentException("the id is not support"));
         /*打桩,构建当userRepository updateUser执行任意User类型的参数,返回的结果都是true*/
        Mockito.when(userRepository.updateUser(Mockito.any(User.class))).thenReturn(true);
        /*打桩,给void方法 */
        Mockito.doAnswer(invocation -> {
            System.out.println("进入Mock");
        return null;
        }).when(userRepository).addUser(Mockito.any());

        /*模拟方法设置返回期望值*/
        List spy = Mockito.spy(new LinkedList<>());
        /*这里会抛出IndexOutOfBoundsException*/
//        Mockito.when(spy.get(0)).thenReturn("foo");
        /*所以要使用下面代码*/
        Mockito.doReturn("foo").when(spy).get(0);
    }



    @Test
    public void testUpdateUsernameSuccess() throws Exception {
        Long userId = 1L;
        String newUsername = "new Jack";
        /*测试service方法*/
        boolean updated = userService.updateUsername(userId,newUsername);
        /*检查结果*/
        Assert.assertThat(updated, Matchers.is(true));

        /*Mock对象一旦创建,就会自动记录自己的交互行为。通过verify(mock).someMethod()方法,来验证方法是否被调用。*/
        /*验证调用上面的service方法后是否 userRepositroy.getOne(1L)调用过。*/
        Mockito.verify(userRepository).getOne(userId);

        /*updateUsername 函数中我们调用了已经打桩了的其他的函数,现在我们来验证进入其他函数中的参数*/
        /*构造参数捕获器,用于捕获方法参数进行验证*/
        ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
        /*验证updateUser方法是否呗调用过,并且捕获入参*/
        Mockito.verify(userRepository).updateUser(userCaptor.capture());
        /*获取参数updateUser*/
        User updateUser = userCaptor.getValue();
        /*验证入参是否是预期的*/
        Assert.assertThat(updateUser.getUsername(),Matchers.is(newUsername));
        /*保证这个测试用例中所有被Mock的对象的相关方法都已经被Verify过了*/
        Mockito.verifyNoMoreInteractions(userRepository);
        /*如果有一个交互没有被verify,则会报错
        org.mockito.exceptions.verification.NoInteractionsWanted:
        No interactions wanted here:
        -> at com.wuwii.service.IUserServiceTest.testUpdateUsernameSuccess(IUserServiceTest.java:74)
        But found this interaction on mock 'iUserRepository':
        -> at com.wuwii.service.impl.UserServiceImpl.findOne(UserServiceImpl.java:21)
        ****/

    }

//    @Test
    public void testUpdateUsernameFailed() throws Exception {
        Long userId = 2L;
        String newUsername = "new Jack";
        /*没有经过mock的updateUser方法,它的返回值是false*/
        boolean updated = userService.updateUsername(userId,newUsername);
        Assert.assertThat(updated,Matchers.is(true));
        /*验证userRepository的getOne(2L)这个方法是否被调用过(这个是被测试过的,此步骤通过)*/
        Mockito.verify(userRepository).getOne(2L);
        /*验证userRepository的updateUser(null)这个方法是否被调用过(这个方法是没有被调用过的)*/
        Mockito.verify(userRepository).updateUser(null);
        Mockito.verifyNoMoreInteractions(userRepository);

    }

创建Mock对象
我们需要对userService进行测试,就需要模拟userRepository对象
我们在setUp()方法中,模拟对象并打桩。

模拟对象有两种方式:

  • 1.对注解@Mock的对象进行模拟MockitoAnnotations.initMocks(this)
  • 2.对单个对象手动Mock: userRepositroy = Mockito.mock(IUserRepositroy.class)

数据打桩
数据打桩,方法非常多,主要分下面几种:

  • 1.最基本的用法就是调用when以及thenReturn方法了。它的作用就是指定当我们调用被代理的对象的某一个方法以及参数的时候,返回什么值。

  • 2.提供参数匹配器,灵活匹配参数。any()any(Class type)anyBoolean()anyByte() anyChar()anyInt()anyLong()等等,它支持复杂的过滤,可以使用正则 Mockito.matches(".*User$"),开头结尾验证 endsWith(String suffix)startsWith(String prefix),判空验证 isNotNull()isNull() 。也还可以使用argThat(ArgumentMatchermatcher),如:ArgumentMatcher只有一个方法boolean matches(T argument);传入入参,返回一个boolean表示是否匹配。Mockito.argThat(argument -> argument.getUsername.length() > 6)

  • 3.Mockito还提供了了两个表示行为的方法:thenAnswer(Answer answer);thenCallRealMethod();分别表示自定义处理调用后的行为,以及调用真实的方法。这两个方法在有些测试用例中还是很有用的。

  • 4.对于同一个方法,Mockito可以是顺序与次数关连的。也就是说可以实现同一个方法 ,第一次调用返回一个值,第二次调用返回一个值,甚至第三次调用抛出异常等等。只需要连续的调用thenXXXX即可。

  • 如果为一个返回Void的方法设置桩数据。上面的方法都是表示的是有返回值的方法,而由于一个方法没有返回值,因此我们不能调用when方法,比如:doAnswer(Answer answer)doNothing()doReturn(Object toBeReturned)doThrow(Class toBeThrown)doCallRealMethod()。它们使用方法其实和上面thenXXXX是一样的.

        /*打桩,给void方法 */
        Mockito.doAnswer(invocation -> {
            System.out.println("进入Mock");
        return null;
        }).when(userRepository).addUser(Mockito.any());
        /*模拟方法设置返回期望值*/
        List spy = Mockito.spy(new LinkedList<>());
        /*这里会抛出IndexOutOfBoundsException*/
//      Mockito.when(spy.get(0)).thenReturn("foo");
        /*所以要使用下面代码*/
        Mockito.doReturn("foo").when(spy).get(0);

验证测试方法的结果
使用断言来检查结果。

验证Mock对象的调用
其实,在这里我们如果只是验证方法结果的正确的话,就非常简单,但是在复杂的方法调用堆栈中,往往可能出现结果正确,但是过程不正确的情况。比如updateUsername方法返回false有两种可能,一直可能是用户没有找到,还有一种可能就是userRepository.updateUser(userPO)返回false。因此如果我们只使用Assert.assertFalse(updated);来验证结果,可能就会忽略某些错误。

因此我们在测试中还需要验证指定的方法userRepository.getOne(userId);是否运行过,而且我们还是用了参数捕获器,抓取中间的方法参数。用来验证。

提供了verify(T mock,VerificationMode mode)方法。VerificationMode有很多作用。

/*验证指定方法 get(3) 没有被调用*/
verify(mock,never()).get(3);

verifyZeroInteractionsverifyNoMoreInteractions验证所有mock的方法是否都调用过了。


MockMvc

MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形势,转换到Controller的调用,是的测试速度快,不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,一共一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilderDefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和继承web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无需启动服务)。MockMvcBuilders提供了对应的创建方法standaloneSetup 方法和webAppContextSetup方法,在使用时直接调用即可。

private MockMvc mockMvc;

@Autowire
private WebApplicationContext webApplicationContext;

@Before
public void setup() {
    /*实例化方式一*/
    mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    /*实例化方式二*/
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

单元测试方法:

  @Test
    public void testHello() throws Exception {
/*
        1.mockMvc.perform 执行一个请求
        2.MockMvcRequestBuilders.get("XXX")构造一个请求
        3.ResultActions.param()
        4.ResultActions.accept()
        5.ResultActions.andExpect
        6.ResultActions.andDo 添加一个结果处理器,表示要对结果做点什么事情。
        7.ResultActions.andReturn 表示执行完成后,返回响应的结果。
*/
        mockMvc.perform(MockMvcRequestBuilders.get("/mock-mvc/test-get")
                /*设置返回类型为utf-8,否则默认为ISO-8859-1*/
                .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .param("name","tom"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("hello"))
                .andDo(MockMvcResultHandlers.print());
    }

整个过程如下
1.准备测试环境
2.通过MockMvc执行请求
3.添加验证断言
4.添加结果处理器
5.得到MvcResult进行分自定义断言/进行下一步异步请求
6.卸载测试环境

注意事项:如果使用DefaultMockMvcBuilder进行MockMvc实例化时需在SpringBoot启动类上添加组件扫描的package的指定,否则会出现404

@ComponentScan(basePackages = "com.creators")

相关API
RequestBuilder提供了一个方法buildRequest(ServletContext servletContext)用于构建MockHttpServletRequest;其中有两个子类MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder(文件上传使用) 。

MockMvcRequestBuilders提供get、post等多种方法用来实例化RequestBuilder。

ResultActions,MockMvc.perform(RequestBuilder requestBuilder)的返回值,提供三种能力:andExpect 添加断言判断结果是否达到预期;andDo,添加结果处理器,比如示例中的打印。andReturn返回验证成功后的MvcResult,用于自定义验证/下一步的异步处理。

一些常用的测试
测试普通控制器

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}",1))
        /*验证存储模型数据*/
       .andExpect(MockMvcResultMatchers.model().attributeExists("user"))
        /*验证viewName*/
       .andExpect(MockMvcResultMatchers.view().name("user/view"))
        /*验证视图渲染时forward到的jsp*/
       .andExpect(MockMvcResultMatchers.forwardedUrl("/WEB-INF/jsp/user/view/jsp"))
        /*验证状态码*/
       .andExpect(MockMvcResultMatchers.status().isOk())
        /*输出MvcResult到控制台*/
       .andDo(MockMvcResultHandlers.print());

得到MvcResult自定义验证

  MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get( "/user/{id}",1)) 
        .andReturn();
  Assert.assertNotNull(result.getModelAndView().getModel().get("user"));

验证请求参数绑定到模型数据及flash属性

mockMvc.perform(MockMvcRequestBuilders.post("/user").param("name","wang"))
                /*验证执行控制器类*/
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                /*验证执行控制器方法名*/
                .andExpect(MockMvcResultMatchers.handler().methodName("create"))
                /*验证页面没有错误*/
                .andExpect(MockMvcResultMatchers.model().hasNoErrors())
                /*验证存在flash属性*/
                .andExpect(MockMvcResultMatchers.flash().attributeExists("success"))
                /*验证视图名称*/
                .andExpect(MockMvcResultMatchers.view().name("redirect:/user"));

文件上传

 byte[] bytes = new byte[]{1,2};
        mockMvc.perform(MockMvcRequestBuilders.multipart("/user/{id}/icon",1L).file("icon",bytes))
                .andExpect(MockMvcResultMatchers.model().attribute("icon",bytes))
                .andExpect(MockMvcResultMatchers.view().name("success"));

JSON请求/响应验证

String requestBody = "{\"id\":1,\"name\":\"wang\"}";
        mockMvc.perform(MockMvcRequestBuilders.post("/user")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(requestBody)
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            /*检查返回JSON数据中某个值的内容: 请参考http://goessner.net/articles/JsonPath/*/
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));

        String errorBody = "{id:1,name:wang}";
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user")
                .contentType(MediaType.APPLICATION_JSON).content(errorBody)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isBadRequest())
                .andReturn();
        Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(mvcResult.getResolvedException().getClass()));

异步测试

  MvcResult mvcResult1 = mockMvc.perform(MockMvcRequestBuilders.get("/user/async?id=1&name=wang"))
                .andExpect(MockMvcResultMatchers.request().asyncStarted())
                .andExpect(MockMvcResultMatchers.request().asyncResult(CoreMatchers.instanceOf(User.class)))
                .andReturn();
        mockMvc.perform(MockMvcRequestBuilders.asyncDispatch(mvcResult1))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));

使用MultiValueMap构建参数

 MultiValueMap params = new LinkedMultiValueMap<>();
        params.add("name","wang");
        params.add("hobby","sleep");
        params.add("hobby","eat");
        mockMvc.perform(MockMvcRequestBuilders.post("/user").params(params));

模拟session和cookie

 mockMvc.perform(MockMvcRequestBuilders.get("/index").sessionAttr("name", "value"));
        mockMvc.perform(MockMvcRequestBuilders.get("/index").cookie(new Cookie("name", "value")));

全局配置

  mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .defaultRequest(MockMvcRequestBuilders.get("/user/1").requestAttr("default",true))
                .alwaysDo(MockMvcResultHandlers.print())
                .alwaysExpect(MockMvcResultMatchers.request().attribute("default",true))
                .build();

如果是测试Service层代码 可以在单元测试方法上加上@Transactional注解,在测试完毕后,数据能自动回滚。
本节全部源码

你可能感兴趣的:(Springboot 单元测试详解)