spring-boot-start-test

Boot starter

SpringBoot提供了spring-boot-start-test启动器,该启动器提供了常见的单元测试库:

  • JUnit: 一个Java语言的单元测试框架

  • Spring Test & Spring Boot Test:为Spring Boot应用提供集成测试和工具支持

  • AssertJ:支持流式断言的Java测试框架

  • Hamcrest:一个匹配器库

  • Mockito:一个java mock框架

  • JSONassert:一个针对JSON的断言库

  • JsonPath:JSON XPath库

JUNIT常用注解

@BeforeClass :在所有测试方法前执行一次,一般在其中写上整体初始化的代码

@AfterClass 在所有测试方法后执行一次,一般在其中写上销毁和释放资源的代码

@Before 在每个测试方法前执行,一般用来初始化方法(比如我们在测试别的方法时,类中与其他测试方法共享的值已经被改变,为了保证测试结果的有效性,我们会在@Before注解的方法中重置数据)

@After 在每个测试方法后执行,在方法执行完成后要做的事情

@Test(timeout = 1000) 测试方法执行超过1000毫秒后算超时,测试将失败

@Test(expected = Exception.class) 测试方法期望得到的异常类,如果方法执行没有抛出指定的异常,则测试失败

@Ignore(“not ready yet”) @Test 执行测试时将忽略掉此方法,如果用于修饰类,则忽略整个类

@RunWith 在JUnit中有很多个Runner,他们负责调用你的测试代码,每一个Runner都有各自的特殊功能,你要根据需要选择不同的Runner来运行你的测试代码。 如果我们只是简单的做普通Java测试,不涉及Spring Web项目,你可以省略@RunWith注解,这样系统会自动使用默认Runner来运行你的代码。

Spring Boot Test常用注解

@RunWith(SpringRunner.class)

JUnit运行使用Spring的测试支持。SpringRunner是SpringJUnit4ClassRunner的新名字,这样做的目的 仅仅是为了让名字看起来更简单一点。

public final class SpringRunner extends SpringJUnit4ClassRunner {

	/**
	 * Construct a new {@code SpringRunner} and initialize a
	 * {@link org.springframework.test.context.TestContextManager TestContextManager}
	 * to provide Spring testing functionality to standard JUnit 4 tests.
	 * @param clazz the test class to be run
	 * @see #createTestContextManager(Class)
	 */
	public SpringRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

@SpringBootTest

该注解为SpringApplication创建上下文并支持Spring Boot特性,其webEnvironment提供如下配置:

Mock-加载WebApplicationContext并提供Mock Servlet环境,嵌入的Servlet容器不会被启动。

RANDOM_PORT-加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个随机端口上监听。

DEFINED_PORT-加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个默认的端口上监听 (application.properties配置端口或者默认端口8080)。

NONE-使用SpringApplication加载一个ApplicationContext,但是不提供任何的servlet环境。

@MockBean

在你的ApplicationContext里为一个bean定义一个Mockito mock。

@SpyBean

定制化Mock某些方法。使用@SpyBean除了被打过桩的函数,其它的函数都将真实返回。

@WebMvcTest

该注解被限制为一个单一的controller,需要利用@MockBean去Mock合作者(如service)。

``@DataJpaTest```

provides some standard setup needed for testing the persistence layer:

  • configuring H2, an in-memory database
  • setting Hibernate, Spring Data, and the DataSource
  • performing an @EntityScan
  • turning on SQL logging

单元测试

. Unit tests are responsible for testing a specific piece of code, just a small functionality (unit) of the code.

Take a look into the CreateClientService class. You'll notice that it has a single dependency on the ClientRepository class. Now take a look into the CreateClientServiceTest and notice how I can mock the dependencies of my CreateClientService easily and execute simple unit tests using zero Spring functionality.

public class CreateClientServiceTest {

    private CreateClientService createClientService;
    private ClientRepository clientRepositoryMock;

    @Before
    public void setUp() {
        clientRepositoryMock = Mockito.mock(ClientRepository.class);
        createClientService = new CreateClientService(clientRepositoryMock);
    }

    @Test
    public void createClientSuccessfuly() throws Exception {
        when(clientRepositoryMock.findByName(eq("Foo"))).thenReturn(Optional.empty());
        doAnswer(returnsFirstArg()).when(clientRepositoryMock).save(any(Client.class));

        Client client = createClientService.createClient("Foo");
        assertEquals("Foo", client.getName());
        assertNotNull(client.getNumber());
    }

    @Test(expected = InvalidClientNameException.class)
    public void createClientWithEmptyName() throws Exception {
        createClientService.createClient("");
    }

    @Test(expected = ClientNameAlreadyExistsException.class)
    public void createClientWithExistingName() throws Exception {
        doThrow(new ClientNameAlreadyExistsException()).when(clientRepositoryMock).findByName(eq("Foo"));

        createClientService.createClient("Foo");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Testing Spring Data repositories

If you look into the ClientRepositoryTest you'll see that we are using the annotation @DataJpaTest. Using this annotation we can have a TestEntityManager injected on the test, and use it to change the database to a state in which it's possible to unit test our repository methods. In the code example, I'm testing a simple findByName() method (that is implemented by Spring and I don't really would test in a real life application). I'm using the injected entity manager to create a Client, and then I'm retrieving the created client using the repository.

@RunWith(SpringRunner.class)
@DataJpaTest
public class ClientRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private ClientRepository clientRepository;

    @Test
    public void testFindByName() {
        entityManager.persist(new Client("Foo"));

        Optional<Client> client = clientRepository.findByName("Foo");
        assertEquals("Foo", client.get().getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Testing Spring MVC controllers

@RunWith(SpringRunner.class)
public class ClientControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    CreateClientService createClientServiceMock;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void testCreateClientSuccessfully() throws Exception {
        given(createClientServiceMock.createClient("Foo")).willReturn(new Client("Foo"));

        mockMvc.perform(post("/clients")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsBytes(new CreateClientRequest("Foo"))))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name", is("Foo")))
            .andExpect(jsonPath("$.number", notNullValue()));
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

集成测试

Spring Boot offers a lot of support for writing integration tests for your application. First of all, you can use the new @SpringBootTest annotation. This annotation configures a complete test environment with all your beans and everything set up. You can choose to start a mock servlet environment, or even start a real one with the webEnvironment attribute of the @SpringBootTest annotation.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CreateClientIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void createClient() {
        ResponseEntity<Client> responseEntity =
            restTemplate.postForEntity("/clients", new CreateClientRequest("Foo"), Client.class);
        Client client = responseEntity.getBody();

        assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
        assertEquals("Foo", client.getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

打包测试用例

package com.freshal.test;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class) 
@SuiteClasses({ParameterTest.class,  JunitTest.class}) 
public class PackageTest {
    // 类中不需要编写代码
}
1
2
3
4
5
6
7
8
9
10
11

关于WebMvcTest和SpringbootTest的区别

@SpringBootTest是个通用的测试注解,会完整的启动应用的context。在SpringBoot1.4后,引入了一个新的注解WebMvcTest,可以仅测试Controller层,需要自行提供Controller的依赖或者使用Mock对象进行测试。

看下面的代码片段

HelloController

package com.freshal.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    HelloService helloService;

    @GetMapping("/")
    public String getHello(){
        return helloService.sayHello();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

HelloService

package com.freshal.springboot;

import org.springframework.stereotype.Service;

@Service
public class HelloService {

    public  String sayHello(){
        return "Greetings from Spring Boot!";
    }
}

1
2
3
4
5
6
7
8
9
10
11
12

如果测试用例这样写

package com.freshal.springboot;

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.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().string(equalTo("Greetings from Spring Boot!")));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

测试用例执行中会报错

Field helloService in com.freshal.springboot.HelloController required a bean of type 'com.freshal.springboot.HelloService' that could not be found.

这是因为WebMvcTest并没有完全的载入Context。

根据Sprinboot的文档说明:

@WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, WebMvcConfigurer, and HandlerMethodArgumentResolver. Regular @Component beans are not scanned when using this annotation.

我们可以采用mock对象的方式对Controller测试,如下:

package com.freshal.springboot;

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.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;

    @MockBean
    HelloService helloService;

    @Test
    public void getHello() throws Exception {
        given(this.helloService.sayHello())
            .willReturn(new String("Greetings from Spring Boot!"));
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().string(equalTo("Greetings from Spring Boot!")));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

或者ComponentScan注解,添加对Service的依赖。

package com.freshal.springboot;

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.context.annotation.ComponentScan;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
@ComponentScan(basePackageClasses=com.freshal.springboot.HelloService.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void getHello() throws Exception {
        //given(this.helloService.sayHello())
        //    .willReturn(new String("Greetings from Spring Boot!"));
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().string(equalTo("Greetings from Spring Boot!")));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

如果使用SpringbootTest注解


package com.freshal.springboot;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest2 {
    @Autowired
    private MockMvc mvc;

    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().string(equalTo("Greetings from Spring Boot!")));
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

关于MockMvc and RestTemplate 在集成测试中的区别

MockMvc 和 RestTemplate都可以用于集成测试

With MockMvc, you're typically setting up a whole web application context and mocking the HTTP requests and responses. So, although a fake DispatcherServlet is up and running, simulating how your MVC stack will function, there are no real network connections made.

With RestTemplate, you have to deploy an actual server instance to listen for the HTTP requests you send.

MockMvc没有Web Server,RestTemplate有Web Server