+3

Part 1: Fundamental of unit test

Trong phần đầu tiên của series All things about unit test with Jest, mình sẽ giới thiệu về các concept cơ bản về unit test.


Đặt vấn đề

  • Unit test đã không còn quá xa lạ đối với bất kì một lập trình viên nào. Tuy nhiên đôi khi những bạn mới bắt đầu vọc vạch và viết unit test nhằm đảm bảo chất lượng phần mềm của mình thì sẽ hơi khó khăn và không biết bắt đầu từ đâu.
  • Như mình khi bắt đầu tìm hiểu về nó cũng rất mơ hồ và confuse khi có quá nhiều lí thuyết mới lạ khi mình mới tiếp xúc 🤒 Vì thế ở bài viết này mình sẽ chia sẻ về quá trình tham khảo tài liệu cũng như kinh nghiệm của mình, để phần nào giúp các bạn có cách tiếp cận dễ dàng hơn với unit test.
  • Ngoài ra trong bài viết này, mình có sử dụng mã một vài nơi sẽ là viết với ngôn ngữ typescript để viết ví dụ được dễ dàngdễ đọc hơn.

Intro

Bài viết này khá dài, vì vậy mình sẽ chia series này thành 4 phần, ở phần đầu này chủ yếu mình sẽ chia sẻ về concept của 1 unit test, vì vậy nếu bạn nào chưa biết thì mình recommend nên đọc trước khi sang các phần khác, nếu bạn đã biết rồi thì có thể skip sang phần 2 nghen 🙇🏼‍♂️

Lý thuyết

1. Unit test là gì?

Unit test là kiểm thử tự động cho một đơn vị mã nguồn (hơi khó hiểu đúng không :D). 1 unit test sẽ xác nhận (assert) liệu hành vi (behavior) của đơn vị có phù hợp với mong đợi (expectation) hay không. Tuy vài dòng khó hiểu như vậy, nhưng thực chất:

Nói ngắn gọn là để kiểm tra chức năng chúng ta viết đúng như mong đợi hay không và sau này có chỉnh sửa chức năng đó thì có thể phát hiện ra lỗi kịp thời nếu có vấn đề phát sinh.

Tổng quan lại thì mục đích của unit test nhằm để đảm bảo ứng dụng của chúng ta hoạt động đúng như mong đợi, góp phần tăng độ tin cậytính ổn định.

2. Test double là gì?

Khái niệm Test double là một kỹ thuật được sử dụng trong unit testing để tạo ra các đối tượng giả để đại diện cho các đối tượng thật trong hệ thống, nhằm kiểm tra việc tương tác giữa chúng và giúp cho việc test có thể được thực hiện dễ dàng hơn. Tại sao phải sử dụng?
Có một vài lý do để sử dụng test double trong unit test như: (skip cũng được skip lí do thôi nhé 😃 )

  • Đảm bảo tính cách biệt trong việc kiểm thử một thành phần riêng lẻ của hệ thống, giúp giảm thiểu các ảnh hưởng không mong muốn đến các thành phần khác.
  • Tạo điều kiện cho việc tái sử dụng các phần kiểm thử, giảm thiểu thời gian và chi phí cho việc phát triển hệ thống.
  • Giúp tách biệt các thành phần của hệ thống để phát triển, test và triển khai một cách độc lập.

Tất nhiên nếu chỉ có vài lí do như vậy thì khá khó nhớ, vậy nên mình sẽ đưa ra ví dụ cho mọi người hiểu rõ hơn
VD: các bạn đang viết một ứng dụng đặt lịch (booking phòng khách sạn) chẳng hạn, thì mỗi lần đặt lịch các bạn sẽ phải call API, tuy nhiên mỗi lần gọi API sẽ phải trả phí và bị limit số lần gọi 😃)

Bạn sẽ nghĩ là ồ, khó nhằn đây, chả nhẽ việc test nó mà mình cũng mất tiền ư, có cách nào mà vẫn đảm bảo được (input, outcome) cần test mà không mất phí không nhỉ 🤨
===> Và đó chính là lí do mà Test Double tới với chúng ta.

Test Double giúp chúng ta giả lập (input, outcome) ~ (đầu vào, đầu ra), và nhờ vậy mà ta có thể control cũng như dễ dàng setup và thực hiện kiểm thử đơn vị. Khá hay đúng không nào, vậy hãy cùng mình jump vào các thành phần trong Test Double:

Các thành phần chính: (gồm 5 thành phần)

Dummy

Dummy object thường chỉ dùng cho việc fill parameter lists trong function hay constructor, và thực vậy nên nó cũng không có tác dụng nào hơn cả 😃)
Dummy cũng thường dùng cho việc ám chỉ rằng ta chỉ tạo ra một vài thứ nhằm để điền cho đủ chứ không nhằm mục đích sử dụng.

VD:

// userAuthen.ts
class UserAuthen {
    constructor(){}
    
    function authorize(username: string, password: string){
        return false;
    }
}

const userAuthen = new UserAuthen();
const dummyUsername = 'John Doe';
const dummyPassword = 'test';
userAuthen.authorize(dummyUsername, dummyPassword);

Oh, bạn thấy đấy, rằng việc bạn phải pass cho đủ số lượng parameter dù cho chúng chẳng dùng để làm gì, thì đó chính xác là những gì mà dummy object làm 😄

Stub

Stub dùng để cung cấp sẵn một kết quả(outcome) cho function mình muốn mock (thường thì hay dùng cho việc mock kết quả của external dependency (các đoạn mã khác mà hàm đang phụ thuộc).

Spy

Spy thì là một stub, tuy nhiên mục đích của nó lại khác, nó dùng để theo dõi các đối tượng trong quá trình thực thi (VD: số lượng đối số, đối số gọi vào, được gọi bao nhiêu lần)

Mock

Mock khá giống spy + stub, nó vừa có thể record information về các đối tượng trong quá trình thực thi, vừa có thể giả định kết quả trả về. Tuy nhiên mục đích thực của nó sẽ là thay thế cho các hàm thật để chạy. Và thường thì nó cũng sẽ không quan tâm đến outcome, mà nó hay dùng với mục đích giống spy hơn.

Fake

Fake object sẽ thực sự có các triển khai hoạt động, và thông thường nó sẽ sử dụng cho một vài trường hợp cần đến 1 vài field trong 1 object.

Cũng khá khó để có thể minh hoạ cho các bạn đầy đủ về stub, spy, mock, fake, hơn nữa bài viết này chỉ nhằm mục đích nêu rõ các concept behind the scene of unit test.
Nhưng đừng lo lắng, ở các bài viết tiếp theo chúng ta sẽ đi qua từng khái niệm một
Mình có để tài liệu tham khảo phía bên dưới bài viết, nếu bạn nào mong muốn hiểu tường mình hơn thì nhấn vào nhé! 😆

Sau khi tìm hiểu một lượt rồi, mình muốn đưa đến kết luận cho các bạn để các bạn có thể thực sự phân biệt được các loại test double (dù trong thực tế khi viết unit test sẽ không còn để ý lắm đến điều đó nữa :v ):

  • We can say that a Mock is a kind of spy, a spy is a kind of stub, and a stub is a kind of dummy.
  • But a fake isn’t a kind of any of them. It’s a completely different kind of test double.

3. Mô hình AAA testing

AAA model là một phương pháp đặt tên và phân chia cấu trúc của unit test, nó bao gồm 3 phần chính:

  • Arrange: phần chuẩn bị dữ liệu, tạo các mock object, stub, fake object, ... để sẵn sàng cho việc test. Phần này nên được đặt ở đầu của test case.
  • Act: phần thực hiện hành động cần được test, ví dụ như gọi một phương thức hay thay đổi một giá trị biến. Phần này cần được đặt ngay sau phần chuẩn bị dữ liệu.
  • Assert: phần kiểm tra kết quả trả về từ phần Act, đảm bảo rằng hành động đã được thực hiện đúng và kết quả trả về đúng như mong đợi. Phần này nên được đặt cuối cùng của test case.

Ví dụ:

function sum(a, b) {
  return a + b;
}

describe('sum function', () => {
  test('should return the correct sum', () => {
    // Arrange
    const a = 2;
    const b = 3;

    // Act
    const result = sum(a, b);

    // Assert
    expect(result).toBe(5);
  });
});

Giải thích:

  • Arrange: Khởi tạo các giá trị cần thiết cho việc test. Trong ví dụ này, chúng ta khởi tạo giá trị a và b là 2 và 3.
  • Act: Thực thi hàm cần test và lấy ra kết quả trả về. Trong ví dụ này, chúng ta gọi hàm sum với tham số a và b và lưu kết quả trả về vào biến result.
  • Assert: So sánh kết quả trả về với giá trị mong đợi. Trong ví dụ này, chúng ta kiểm tra xem kết quả trả về của hàm sum có phải là 5 hay không bằng cách sử dụng expect của Jest. Có thể thấy phương pháp AAA giúp cho test case được phân chia rõ ràngdễ hiểu, giúp cho việc đọc và sửa lỗi test case dễ dàng hơn. Ngoài ra, phương pháp này cũng đảm bảo việc chuẩn bị và thực hiện các bước test case được độc lập với nhau, giúp tăng tính ổn địnhđộ tin cậy của unit test.

Kết luận

Trong phần 1 này, chúng ta đã cùng đi qua các concept quan trọng hình thành nên unit test cũng như test double.
Cảm ơn các bạn đã dành thời gian đọc bài viết, hẹn gặp lại vào các bài viết tiếp theo ^_^.

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí