Viblo Learning
+2

Unit RestControllers in Spring Boot

Bài viết này mình giới thiệu một vài cách tiếp cận để viết test trong Spring Boot. Bài viết này chỉ tập trung vào tầng Controller - nó cũng là tầng test không rõ ràng nhất.

Server and Client Side Tests

Để bắt đầu, ta sẽ chia thành 2 phần là server-side và client-side tests.
Server-side tests là ta thực hiện Request của mình và ta muốn kiểm tra cách máy chủ hoạt động, các thành phần phản hồi, nội dung phản hồi, ....
Client-side tests thì ít phổ biến hơn. Chúng ta cũng sẽ chỉ tập trung server-side tests, ta sẽ mock (giả lập) các requests.

Server side Tests

Test tầng Controller cũng có 2 hướng tiếp cận. Một là sử dụng MockMVC, hai là sử dụng RestTemplate. Nếu là viết Unit test thì nên sử dụng MockMVC, nếu là viết Intergation test thì sử dụng RestTemplate.

The sample application

  • Nếu một SuperHero không được tìm thấy bởi định danh của nó, một NonExistingHeroException được ném. Có một annotation của Spring là @RestControllAdvice sẽ chặn ngoại lệ đó và biến nó thành mã trạng thái 404 - NOT_FOUND.
  • Có một lớp SuperHeroFilter sẽ được sử dụng trong giao tiếp HTTP để thêm tiêu đề vào HTTP Response: X-SUPERHERO-APP.

Strategy 1: MockMVC in Standalone Mode

Sử dụng MockMVC trong chế độ độc lập (Standalone Mode) sẽ không load bất kỳ context nào.

@RunWith(MockitoJUnitRunner.class)
public class SuperHeroControllerMockMvcStandaloneTest {
 
    private MockMvc mvc;
 
    @Mock
    private SuperHeroRepository superHeroRepository;
 
    @InjectMocks
    private SuperHeroController superHeroController;
 
    // This object will be magically initialized by the initFields method below.
    private JacksonTester<SuperHero> jsonSuperHero;
 
    @Before
    public void setup() {
        // We would need this line if we would not use MockitoJUnitRunner
        // MockitoAnnotations.initMocks(this);
        // Initializes the JacksonTester
        JacksonTester.initFields(this, new ObjectMapper());
        // MockMvc standalone approach
        mvc = MockMvcBuilders.standaloneSetup(superHeroController)
                .setControllerAdvice(new SuperHeroExceptionHandler())
                .addFilters(new SuperHeroFilter())
                .build();
    }
 
    @Test
    public void canRetrieveByIdWhenExists() throws Exception {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));
 
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo(
                jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
        );
    }
 
    @Test
    public void canRetrieveByIdWhenDoesNotExist() throws Exception {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willThrow(new NonExistingHeroException());
 
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
        assertThat(response.getContentAsString()).isEmpty();
    }
 
    @Test
    public void canRetrieveByNameWhenExists() throws Exception {
        // given
        given(superHeroRepository.getSuperHero("RobotMan"))
                .willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));
 
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/?name=RobotMan")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo(
                jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
        );
    }
 
    @Test
    public void canRetrieveByNameWhenDoesNotExist() throws Exception {
        // given
        given(superHeroRepository.getSuperHero("RobotMan"))
                .willReturn(Optional.empty());
 
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/?name=RobotMan")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo("null");
    }
 
    @Test
    public void canCreateANewSuperHero() throws Exception {
        // when
        MockHttpServletResponse response = mvc.perform(
                post("/superheroes/").contentType(MediaType.APPLICATION_JSON).content(
                        jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
                )).andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
    }
 
    @Test
    public void headerIsPresent() throws Exception {
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getHeaders("X-SUPERHERO-APP")).containsOnly("super-header");
    }
}
  • MockitoJUnitRunner để chạy các unit test. Nó được cung cấp bởi Mockito.
    Lưu ý cách ta giả lập SuperHeroRepository bằng annotaion @Mock. Truy nhiên ta cần nó bên trong lớp controller thật, vì vậy ta cần @InjectMocks lớp SuperHeroController. Bằng cách này repository được mock sẽ được tiêm vào controller thay vì bean thực.
    Đối với mỗi test, ta sử dụng một MockMVC instance để thực hiện các loại fake requests ta cần test như (GET, POST, ...). Và ta nhận về MockHttpServletResponce trả về. Hãy nhở rằng, đây không phải response thực sử, mọi thứ chỉ đang là mô phỏng, giả lập.

JacksonTester initialization

JacksonTesterobject cũng được tiêm tự động vào đây bằng cách sử dụng phương thức JacksonTester.initFields().

Configure the Standalone Setup in MockMVC

Đặt annotation @Before trên setup method, thì phần thân bên trong method này sẽ được thực thi trước khi mỗi test được thực thi. Ta cần cấu hình MockMvc ở chế độ độc lập (standalone mode), cấu hình rõ ràng controller, controller advice và HTTP filter.

Testing ControllerAdvices and Filters with MockMVC

assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
Khi test pass qua dòng code trên điều đó chứng tỏ ControllerAdvice hoạt động tốt. Ta cũng có một method test chứng tỏ Filter hoạt động tốt. Bây giờ xóa bỏ phần thiết lập chế độ độc lập và chạy lại test sẽ thấy test fail, bởi vì không có context nào để inject vào class.
Nhưng việc kiểm tra header và mã lỗi 404 đúng với Intergation test hơn. Vì vậy để test unit test đơn thuần thì ta hãy xóa phần setup ControllerAdvice và Filter khỏi setup chế độ độc lập. Như vậy ta sẽ có kịch bản unit test controller thuần túy.

Strategy 2: MockMVC with WebApplicationContext

Trường hợp này ta sẽ load cả Spring’s WebApplicationContext.

@RunWith(SpringRunner.class)
@WebMvcTest(SuperHeroController.class)
public class SuperHeroControllerMockMvcWithContextTest {
 
    @Autowired
    private MockMvc mvc;
 
    @MockBean
    private SuperHeroRepository superHeroRepository;
 
    // This object will be magically initialized by the initFields method below.
    private JacksonTester<SuperHero> jsonSuperHero;
 
    @Before
    public void setup() {
        // Initializes the JacksonTester
        JacksonTester.initFields(this, new ObjectMapper());
    }
 
    @Test
    public void canRetrieveByIdWhenExists() throws Exception {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));
 
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
 
        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo(
                jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
        );
    }
 
    // ...
    // Rest of the class omitted, it's the same implementation as in Standalone mode
 
}

SpringRunner

Config test với @SpringRunner, các context sẽ được load và các bean sẽ được inject khi thực thi test.

MockMVC Autoconfiguration

Với chú thích @WebMVCTest, đối tượng MockMVC của sẽ được cấu hình tự động và có sẵn trong context.
Việc sử dụng annotation đủ thông minh để biết rằng Filter và ControllerAdvice cũng nên được thêm vào, vì vậy, trong trường hợp này, không có cấu hình rõ ràng trong phương thức setup().

Overriding beans for testing using MockBean

Bây giờ, repository được inject trong ngữ cảnh Spring, sử dụng @MockBean. Ta cũng không cần phải tạo bất kỳ tham chiếu nào đến controller, vì lúc Bean này sẽ thay thế cho Bean repository thật.

No server calls

Các reponse mà chúng ta xác minh vẫn là giả mạo. Không có máy chủ web liên quan đến thử nghiệm này.


All Rights Reserved