Bài viết này sẽ hướng dẫn các bạn thực hiện Integration Test cho các API Spring Boot

1. Giới thiệu

Hoạt động Testing luôn đóng vai trò quan trọng trong việc phát triển phần mềm, nó đóng vai trò như là 1 cửa chặn cuối đảm bảo tất cả những gì chúng ta đã làm ra sẽ hoạt động đúng như yêu cầu và thiết kế. Với những Backend Dev thực hiện thiết kế và implement tầng API lại càng quan trọng hơn.

Tuy nhiên có một thực tế là, việc kiểm thử API có khá nhiều khó khăn, không dễ dàng và trực quan như thực hiện Test UI. Chúng ta thường thường phải chờ các API được deploy lên các môi trường như Testing, CI để các component (user) khác gọi vào để kiểm tra xem API có hoạt động đúng không, nếu có lỗi xảy ra Backend Dev sẽ đi fix lỗi và lại phải chờ deploy lên môi trường Test. Như vậy sẽ tốn không ít thời gian cho quy trình: deploy, testing, report, fix lỗi. Một số bạn có thể dùng Postman để chủ động hơn để selft-test API của mình tuy nhiên Postman lại khá khó khăn cho việc tổ chức các Test case, theo các Test Suite hoặc theo nhóm API và chỉ xuất được output cuối cùng của API dẫn đến khá khó khăn trong việc trace code lỗi.

Do đó, Spring Framework cung cấp công cụ Spring Testing cho các Backend Dev có thể thực hiện Integration Test ngay trên môi trường local, gần như cover được hết những case exception có thể xảy ra. Bài viết này sẽ hướng dẫn các bạn giả lập 1 API trả về các lỗi Bad Request 400, Internal Server Error 500, và trả về kết quả thành công.

2. Tools

Bài viết sử dụng các tools sau:

  • Java JDK 8
  • Spring Boot 2.0.5
  • Spring Test 2.0.5
  • Maven 3

2. Build thư viện Maven 

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <version>2.0.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.4</version>
        </dependency>
    </dependencies>

3. Xây dựng Controller

Giả sử chúng ta có nghiệp vụ như sau: người dùng sẽ đăng ký nhận các Newsletter mỗi khi Hệ Thống hay dịch vụ của bạn có cập nhật dịch vụ mới hay những chương trình khuyến mãi. Người dùng sẽ nhập vào email và họ tên đầy đủ.

API của chúng ta sẽ xử lý nghiệp vụ này như sau:

  1.  Success: nguời dùng nhập email và họ tên hợp lệ, email không trùng với email đã có trong Hệ thống.
  2.  Bad Request:  người dùng nhập thiếu 1 trong 2 param email hoặc tên đầy đủ.
  3. Not Acceptable: email và họ tên được nhập đầy đủ, nhưng email bị trùng với email có sẵn trong hệ thống.

 API của chúng ta như sau:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;

@RestController
@RequestMapping(value = "/api/newsletter/v1")
public class NewsletterController {
    private static final String[] REGISTERD_EMAILS = new String[]{"abcd@123.com", "admin@thecoderoot.com"};

    @PostMapping(value = "/register")
    public ResponseEntity<String> registerNewsletter(@RequestParam(value = "email") String email,
                                                     @RequestParam(value = "fullName") String fullName){
        //if email is duplicated, an exception is thrown
        if (Arrays.asList(REGISTERD_EMAILS).contains(email)){
            return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body("This email has been registered");
        }

        //handle register a new newsletter
        System.out.println("Registered: " + email + " - " + fullName);
        return ResponseEntity.status(HttpStatus.OK).body("OK");
    }
}

 

Ở đây chúng ta sẽ giả lập:

  • Xử lý email và họ tên bằng cách println ra console.
  • Giả lập 1 loạt email hardcode có sẵn.

Trong thực tế chúng ta sẽ phải ít nhất check email có bị trùng bằng cách đọc DB kiểm tra và lưu dữ liệu xuống database.

4. Testing 

Chúng ta sử dụng thư viện Mockmvc để gọi vào các API của code Controller. 

Về bản chất, khi bắt đầu thực hiện 1 test case thì thư viện Mockmvc sẽ thực hiện start project như việc chạy gói jar trên môi trường thật, điều đó có nghĩa là các test case của chúng ta sẽ được giả lập gần giống như client gọi vào các API thật vậy, chỉ khác là nếu có lỗi xảy ra chúng ta sẽ trace lỗi dễ dàng hơn nhờ các output log được xuất ra trong quá trình chạy Test.

Chúng ta sẽ lần lượt thực hiện các Test case như sau:

package thecoderoot.spring.validation.controller;


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.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 org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@RunWith(SpringRunner.class)
@WebMvcTest
@AutoConfigureMockMvc
public class NewsletterControllerTest {
    @Autowired
    private MockMvc mockMvc;

    private static final String REGISTER_URL = "/api/newsletter/v1/register";

    @Test
    public void testRegisterNewsletter_HappyCase() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        paramBuilder.append("email").append("=").append("abc@456").append("&");
        paramBuilder.append("fullName").append("=").append("Nguyen Tran Teo");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    public void testRegisterNewsletter_BadRequest() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        paramBuilder.append("email").append("=").append("abc@456").append("&");
        // paramBuilder.append("fullName").append("=").append("Nguyen Tran Teo");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());
    }

    @Test
    public void testRegisterNewsletter_DuplicateEmail() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        paramBuilder.append("email").append("=").append("admin@thecoderoot.com").append("&");
        paramBuilder.append("fullName").append("=").append("Admin");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isNotAcceptable());
    }
}

Happy case 200 - đăng ký thành công:

    @Test
    public void testRegisterNewsletter_HappyCase() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        paramBuilder.append("email").append("=").append("abc@456").append("&");
        paramBuilder.append("fullName").append("=").append("Nguyen Tran Teo");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

Bad Request 400 - Thiếu param đầu vào:

    @Test
    public void testRegisterNewsletter_BadRequest_Email() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        // paramBuilder.append("email").append("=").append("abc@456").append("&");
        paramBuilder.append("fullName").append("=").append("Nguyen Tran Teo");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());
    }
    
    @Test
    public void testRegisterNewsletter_BadRequest_FullName() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        paramBuilder.append("email").append("=").append("abc@456").append("&");
        // paramBuilder.append("fullName").append("=").append("Nguyen Tran Teo");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());
    }

Not Acceptable 406 (email đã được đăng ký trong hệ thống)

    @Test
    public void testRegisterNewsletter_DuplicateEmail() throws Exception {
        StringBuilder paramBuilder = new StringBuilder("?");
        paramBuilder.append("email").append("=").append("admin@thecoderoot.com").append("&");
        paramBuilder.append("fullName").append("=").append("Admin");

        mockMvc.perform(MockMvcRequestBuilders.post(REGISTER_URL + paramBuilder.toString())
                .contentType(MediaType.TEXT_PLAIN))
                .andExpect(MockMvcResultMatchers.status().isNotAcceptable());
    }

Một điều tiện lợi: Khi sử dụng Spring Test, bạn phát hiện có lỗi trong code implementation của mình và tiến hành sửa nó, sau đó bạn không cần phải tiến hành build và redeploy lại gói web mà chỉ cần chạy lại test case là được. Test case sẽ lập tức tiến hành test ngay trên code vừa mới cập nhật và bạn có thể thấy ngay kết quả, điều này làm cho quá trình development và chỉnh sửa trở nên dễ dàng hơn và tiết kiệm thời gian hơn. Lập trình viên có thời gian để tập trung vào việc xử lý các logic của hệ thống mà bớt đi sự phiền phức cho các task build, chờ và retest.

5. Tổng kết:

Sử dụng Spring Test giúp các Backend Dev không chỉ tiết kiệm được thời gian development do không cần phải recompile và redeploy lại app trong quá trình testing mà còn chủ động được quá trình sel-test trên chính môi trường local không cần phải deploy. Mặt khác, áp dụng Spring Test còn giúp quản lý dễ dàng các Test Case được viết trong chính project đang phát triển, điều này giúp cho các lỗi sẽ được phát hiện sớm trong quá trình build app, dẫn đến việc phải pass hết tất cả các test case này trước khi quá trình đóng gói phần mềm là bắt buộc phải thực hiện trong quy trình dự án.

Chúc các bạn thành công.

minhnhatict@gmail.com