Đã code là phải test

Phát triển hướng kiểm thử (Test-driven development - TDD) là một quy trình phát triển phần mềm dựa trên sự lặp lại của một chu kỳ phát triển rất ngắn: thay cho các yêu cầu kỹ thuật là các trường hợp kiểm thử thật cụ thể, sau đó phần mềm được cải thiện chỉ để vượt qua các bài kiểm thử mới. Điều này trái ngược với phát triển phần mềm mà cho phép thêm tính năng vào mà chưa được chứng minh là đáp ứng được yêu cầu. (Theo wiki) Đối với các dự án của mình khi thực hiện, yêu cầu rất khắt khe, một dự án được coi là có chất lượng phát triển tốt chỉ được phép có 1 bug / 10 nghìn dòng code trước khi đưa lên môi trường thật mà thôi.

Những bước cơ bản

TDD cơ bản bao gồm các bước:

  1. Coding: tạo ra các mã nguồn
  2. Write test: viết các mã kiểm thử, các mã này có thể là Unitest, Integration Test hay bất kì loạt test nào mà bạn đã từng biết đến
  3. Thực hiện việc test: ở bước này chúng ta sẽ tạo ra các dữ liệu đầu vào và các dữ liệu kết quả mà chúng ta mong đợi, nếu kết quả sau khi thực thi và kết quả chúng ta mong đợi trung khớp nhau, nghĩa là mã nguồn chúng ta viết là chính xác, ngược lại mã nguồn chúng ta viết là chưa đúng
  4. Refactor: nếu mã nguồn chúng ta viết chưa đúng, chúng ta sẽ cần sửa lại cho đến khi nào tạo ra được kết quả như mong đợi thì thôi.

Đối với các dự án của mình đang thực hiện, thông thường mình sẽ có 4 loại test:

  1. Unit test: kiểm thử cho từng hàm nhỏ, để đảm bảo tất cả logic ở các hàm nhỏ được chính xác. Unit test cũng sẽ đảm bảo được tối thiểu 80% độ chính xác cho của chương trình
  2. Integration test: kiểm thử ở mức API, ở bước này chúng ta sẽ cần dựng môi trường y chang môi trường sẽ triển khai và gọi API, những dữ liệu nào không thể dựng được môi trường thì sẽ được giả lập (mockup)
  3. Monkey test: kiểm thử các API sau khi đã được deploy lên môi trường Alpha hoặc Beta, dữ liệu cũng sẽ là dự liệu thật chứ không còn là giả lập nữa
  4. Stress test: để kiểm tra các API đã thoả mãn các yêu cầu về hiệu năng hay chưa

Unit test

Trong phạm vi của bài viết này, mình sẽ chỉ đề cập đến unit test thôi, những loại test khác, mình sẽ đề cập ở những bài viết khác, chi tiết hơn về Unit test, bạn có thể đọc bài viết này nhé

Unit test quan trọng thế nào?

Unit test là công việc dễ dàng được thực hiện với dev, với các thư viện mockup dữ liệu tuyệt vời như Mockito hay MockK, sẽ giúp chúng ta điều hướng được qua tất cả các trường hợp cần test, và kết quả chúng ta sẽ nhận được một báo cáo rất trực quan như thế này.

Bắt tay vào test

Chúng ta hãy lấy dự án quản lý sách làm ví dụ nhé, nghiệp vụ của chúng ta đó là thêm 1 cuốn sách vào cơ sở dữ liệu, chi tiết như sau:

  • Mỗi một tác giả sẽ không thể có 2 cuốn sách cùng 1 tên
  • Sách được thêm sẽ cần phải có tác giả và danh mục hợp lệ (tồn tại trong cơ sở dữ liệu)
Cấu hình dự án

Dự án của chúng ta sẽ sử dụng gradle, junit 5, mockito, test-util và jacoco, và cấu hình sẽ như sau:

dependencies {
    testImplementation 'com.tvd12:test-util:' + testUtilVersion
    testImplementation 'org.mockito:mockito-junit-jupiter:3.11.2'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
    useJUnitPlatform()
    finalizedBy jacocoTestReport
}

Bạn có thể tham khảo source code đầy đủ tại build.gradle file

Test trường hợp thêm mới sách thành công

Chúng ta sẽ cần khởi tạo một lớp test thế này:

@ExtendWith(MockitoExtension.class)
public class BookServiceTest {

    @Mock
    private AuthorRepository authorRepository;
    @Mock
    private BookRepository bookRepository;
    @Mock
    private CategoryRepository categoryRepository;
    @Mock
    private EntityToDataConverter entityToDataConverter;
    @Mock
    private DataToEntityConverter dataToEntityConverter;
    @InjectMocks
    private BookService sut;

    @Test
    public void addBookSuccess() {
        // test source code
    }
}

Bởi vì trong môi trường unit test chúng ta sẽ không kết nối đến cơ sở dữ liệu nên chúng ta sẽ cần mockup dữ liệu, chính vậy mà chúng ta nhìn thấy rất nhiều annotation @Mock

Bởi vì là trường hợp thêm sách thành công nên chúng ta cần mockup như sau:

// given
when(
    bookRepository.findByNameAndAuthorId(
        addBookData.getBookName(),
        addBookData.getAuthorId()
    )
).thenReturn(null); // không có cuốn sách nào của tác giả x với tên y

// tồn tại tác giả
when(authorRepository.findById(addBookData.getAuthorId())).thenReturn(author);

// tồn tại danh mục
when(categoryRepository.findById(addBookData.getCategoryId())).thenReturn(category);

Phần quan trọng nhất đó chính là xác nhận kết quả sau khi được thực thi, phải trùng khớp với kết quả chúng ta mong đợi:

// when
BookData actual = sut.addBook(addBookData);

// then
assertThat(actual).isEqualsTo(bookData);  

Phần quan trọng không kém đó là việc xác nhận các hàm chúng ta đã giả lập phải được chạy qua:

verify(bookRepository, times(1)).findByNameAndAuthorId(
    addBookData.getBookName(),
    addBookData.getAuthorId()
);

verify(authorRepository, times(1)).findById(
    addBookData.getAuthorId()
);

verify(categoryRepository, times(1)).findById(
    addBookData.getCategoryId()
);

Source đầy đủ bạn có thể tham khảo file BookServiceTest

Test trường hợp thêm mới sách thất bại

Chúng ta sẽ test trường hợp thêm mới sách thất bại do đã có một cuốn sách cùng tên và cùng tác giả trước đó rồi:

@Test
public void addBookFailedDueToBookRepository() {
  // given
  final AddBookData addBookData = randomAddBookData();

  when(
      bookRepository.findByNameAndAuthorId(
          addBookData.getBookName(),
          addBookData.getAuthorId()
      )
  ).thenReturn(book); // tồn tại một cuốn sách với tên x của tác giả y

  // when
  // vì tồn tại sách rồi nên kết quả chúng ta nhận được sẽ là một exception
  final Throwable throwable = assertThrows(() -> sut.addBook(addBookData));

  // then
  // kiểm tra exception có trùng khớp với cái ta mong đợi không
  assertTrue(throwable instanceof DuplicatedBookException);

  verify(bookRepository, times(1)).findByNameAndAuthorId(
      addBookData.getBookName(),
      addBookData.getAuthorId()
  );
  validateMockitoUsage();
}

Source đầy đủ bạn có thể tham khảo file BookServiceTest

Nhìn lại kết quả

Kết quả là tổng số case chúng ta đã test được sẽ được ghi ra file: build/reports/jacoco/test/html/com.tvd12.ezydata.example.jpa.service/BookService.html

Tổng kết

Test-driven development - TDD là một trong những quy trình phát triển phần mềm vô cùng quan trọng mà hầu hết các công ty và tập đoàn đều phải áp dụng để nâng cao được chất lượng sản phẩm của mình. Tuy nhiên bên cạnh đó thì cũng có rất nhiều các công ty vì không đủ nguồn lực, hoặc các công ty outsource muốn tiết kiệm chi phí nên đã bỏ qua TDD, nhưng rốt cuộc nó có tiết kiệm được chi phí không? Khi mà tỉ lệ bug tăng lên và thời gian và nguồn lực fix bug cũng vì thế mà tăng theo. Nên theo mình TDD nên là điều bắt buộc. Có rất nhiều bước kiểm thử cần được đưa vào TDD, tuy nhiên theo mình unit test vẫn là quan trọng nhất, nó rất đơn giản để thực hiện nhưng hiệu quả lại cực kì to lớn, mình cũng có viết các tool để tự động tạo ra mã Unit Test, mình sẽ giới thiệu ở các bài sau nhé.

Tham khảo

  1. Wiki
  2. BookServiceTest
  3. Unit test