+2

Writing Great Unit Tests

Unit Test

1. Writing Great Unit Tests

Bài viết được dịch từ blog của Steven Sanderson và một số kinh nghiệm cá nhân của mình.

Bài viết này nhắm đến những lập trình viên có ít nhất một số kinh nghiệm trong việc sử dụng unit test. Nếu bạn chưa bao giờ viết một unit test, xin vui lòng tìm đọc các bài viết giới thiệu về Unit test trên Viblo trước nhé!

2. Unit test là gì và nó có quan trọng không?

Theo như định nghĩa:

...unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use...

Chúng ta có thể hiểu một cách ngắn gọn: các unit test kiểm tra mỗi đơn vị (unit) code của bạn một cách riêng biệt.


Vậy: **_mục đích của Unit Test không phải là để tìm bug như chúng ta lầm tưởng._**

2.1 Unit test không phải dùng để tìm bug

Cá nhân mình rất ủng hộ sử dụng unit test trong quá trình phát triển ứng dụng. Tuy nhiên, bạn cần hiểu được vai trò của unit test trong quy trình Test Driven Development (TDD), và bỏ đi bất kỳ quan niệm sai lầm rằng các unit test là để kiểm thử và tìm kiếm bug.


Vì vậy, nếu bạn đang cố gắng tìm kiếm bug, thì có một cách hiệu quả hơn nhiều đó là hãy chạy toàn bộ ứng dụng với nhau như nó sẽ chạy trong môi trường thực tế, giống như bạn làm công việc kiểm tra thủ công một cách tự nhiên vậy. Nếu bạn tự động hóa kiểu test này để phát hiện những sai sót khi chúng xảy ra trong tương lai, đây được gọi là integration testing và thường sử dụng các kỹ thuật và công nghệ khác hơn là unit test.


Dưới đây là một bảng ví dụ về việc sử dụng các kĩ thuật hợp lý ứng với từng mục tiêu:
Goal Strongest technique
Finding bugs (things that don't work as you want them to) Manual testing (sometimes also automated integration tests)
Detecting regressions (things that used to work but have unexpectedly stopped working) Automated integration tests (sometimes also manual testing, though time-consuming)
Designing software components robustly Unit testing (within the TDD process)

Dịch

Mục tiêu Kỹ thuật mạnh nhất
Tìm kiếm bug (những thứ không làm việc như bạn muốn) Kiểm thử thủ công (đôi khi cả kiểm thử tích hợp tự động)
Phát hiện hồi quy (những thứ đang làm việc tốt nhưng đã bất ngờ ngừng hoạt động) Các kiểm thử tích hợp tự động (đôi khi cũng bao gồm cả kiểm thử thủ công, mặc dù khá tốn thời gian)
Thiết kế các thành phần trong phần mềm mạnh mẽ Unit testing (trong quy trình TDD)



Lưu ý: có một ngoại lệ, nơi các unit test rất hiệu quả trong việc phát hiện bug. Đó là khi bạn đang tái cấu trúc code của một đơn vị (unit) nhưng không có nghĩa là để thay đổi hành vi của nó. Trong trường hợp này, các unit test có thể cho bạn biết nếu hành vi của đơn vị (unit) đó có thay đổi hay không.

2.2. Vậy nếu unit test không phải để tìm bug, thì nó để làm gì?

Tôi cá là bạn đã nghe câu trả lời này hàng trăm lần rồi, nhưng vì quan niệm sai lầm cố hữu trong kiểm thử cứ treo lơ lửng trong tâm trí của các nhà phát triển phần mềm, tôi sẽ nói lại cái nguyên tắc của nó. Một bậc thầy về TDD đã nói rằng, "TDD là một quá trình thiết kế, chứ không phải là một quá trình kiểm thử". Hãy để tôi giải thích thêm: TDD là một cách mạnh mẽ trong việc thiết kế các thành phần phần mềm ("units") tương tác để hành vi của chúng được xác định thông qua các unit test. Tất cả chỉ có vậy!

2.3. Những unit test tốt vs dở

TDD sẽ giúp bạn cung cấp các thành phần phần mềm hoạt động theo thiết kế của bạn. Một bộ unit test tốt là vô cùng có giá trị: nó là tài liệu thiết kế của bạn, khiến cho việc tái cấu trúc và mở rộng code của bạn được dễ dàng hơn trong khi giữ lại một cái nhìn tổng quan rõ ràng về hành vi của mỗi thành phần. Tuy nhiên, một bộ unit test tồi là vô cùng đau thương: nó chẳng chứng tỏ bất cứ điều gì rõ ràng cả, và có thể làm cản trở nghiêm trọng khả năng tái cấu trúc hoặc thay đổi code của bạn theo bất kỳ cách nào.

Các kiểm thử của bạn nằm ở đâu trên thang mức độ sau? Vai trò của unit test trong phát triển ứng dụng.

Các unit test được tạo ra thông qua quy trình TDD dĩ nhiên nằm ở cực trái của thang mức độ ở trên. Chúng chứa rất nhiều kiến thức về hành vi của một đơn vị code riêng lẻ. Nếu hành vi của đơn vị đó thay đổi, vì vậy phải thực hiện unit test cho đơn vị đó, và ngược lại. Nhưng chúng không có bất kỳ kiến thức hoặc giả định về các phần khác trong codebase của bạn, nên những thay đổi ở các bộ phận khác trong codebase của bạn không làm chúng bị lỗi (và nếu chúng bị lỗi, thì điều đó cho thấy chúng không phải là những unit test đúng). Do đó việc duy trì chúng khá dễ dàng, và như là một kỹ thuật phát triển, TDD có thể áp dụng cho bất kỳ kích thước của dự án nào.

Ở đầu kia của thang mức độ, các kiểm thử tích hợp (integration test) không biết về cách codebase của bạn được chia nhỏ thành các đơn vị (unit), nhưng thay vào đó nó cho thấy cách toàn bộ hệ thống ứng xử đối với người dùng bên ngoài như thế nào. Chúng có chi phí hợp lý để duy trì (vì không quan trọng việc bạn tái cấu trúc hoạt động nội bộ trong hệ thống của bạn như thế nào, nó sẽ không ảnh hưởng đến hành vi bên ngoài) và chúng chứng tỏ được rất nhiều về những tính năng thực sự làm việc hiện nay.

Bất cứ nơi nào ở giữa thang mức độ trên, nó không rõ ràng về những giả định mà bạn đang làm và những gì bạn đang cố gắng chứng minh. Việc tái cấu trúc có thể làm hỏng những kiểm thử này, hoặc cũng có thể không, bất kể trải nghiệm của người dùng vẫn hoạt động. Việc thay đổi các dịch vụ bên ngoài mà bạn sử dụng (chẳng hạn như việc nâng cấp cơ sở dữ liệu) có thể làm hỏng những kiểm thử này, hoặc cũng có thể không, bất kể trải nghiệm của người dùng vẫn hoạt động. Bất kỳ thay đổi nhỏ trong các hoạt động nội bộ của một đơn vị riêng lẻ có thể buộc bạn phải sửa lại hàng trăm hybrid test mà dường như chẳng liên quan gì cả, vì vậy chúng có xu hướng tiêu tốn một lượng lớn thời gian để bảo trì - đôi khi nó gấp 10 lần số lượng thời gian mà bạn dùng để bảo trì phần code thực sự của ứng dụng đó. Và nó cũng gây nản lòng vì bạn biết rằng việc bổ sung thêm nhiều điều kiện tiên quyết để thực hiện các hybrid test chuyển sang màu xanh thì không thực sự chứng tỏ được điều gì cả.

2.4. Làm thế nào để viết những unit test tốt

Vừa rồi là những thảo luận khá mơ hồ - và đây là lúc để đưa ra một số lời khuyên thiết thực. Dưới đây là một số hướng dẫn để viết các unit test mà luôn nằm ở vị trí Sweet Spot Acủa thang mức độ ở trên, và cũng có những giá trị theo cách khác nữa.

  • Hãy làm cho mỗi test độc lập với tất cả những phần khác

Bất kỳ hành vi nhất định nào cũng nên được xác định tại một và chỉ duy nhất một test. Nếu không, sau này khi bạn thay đổi hành vi đó, bạn sẽ phải thay đổi rất nhiều test. Các hệ quả của nguyên tắc này bao gồm:

Loại bỏ những assertion không cần thiết Những hành vi cụ thể nào mà bạn đang kiểm thử? Nó sẽ gây phản tác dụng nếu cứ Assert() bất cứ thứ gì mà cũng đã được assert bằng test khác: nó chỉ làm tăng tần suất của những thất bại vô nghĩa mà chẳng cải thiện unit test một chút nào cả. Điều này cũng áp dụng cho việc gọi Verify() không cần thiết - nếu nó không phải là hành vi cốt lõi dưới test đó, thì hãy dừng việc quan sát về nó! Đôi khi, cộng đồng TDD thường diễn đạt điều này bằng cách nói rằng chỉ có duy nhất một assertion hợp lý trên mỗi test.

Hãy nhớ rằng, các unit test là một thiết kế chỉ rõ một hành vi nào đó sẽ làm việc như thế nào, không phải là một danh sách các quan sát của tất cả mọi thứ mà phần code đó thực hiện.

Kiểm thử chỉ một unit code tại một thời điểm Kiến trúc của bạn phải hỗ trợ việc testing units (tức là, các class hoặc mọi nhóm rất nhỏ của các class) độc lập, chứ không phải là tất cả chúng kết lại với nhau. Nếu không, bạn sẽ có rất nhiều chồng chéo giữa các test, vì vậy việc thay đổi một unit ảnh hưởng ra bên ngoài và gây ra thất bại ở khắp mọi nơi.

Nếu bạn không thể làm được điều này, thì kiến trúc của bạn đang làm hạn chế chất lượng công việc của bạn - hãy xem xét sử dụng Inversion of Control.

Giả lập tất cả những dịch vụ và trạng thái bên ngoài Nếu không, hành vi trong những dịch vụ bên ngoài sẽ chồng chéo lên nhiều test, và trạng thái dữ liệu khác nhau trong mỗi unit test có thể ảnh hưởng đến kết quả của nhau.

Bạn chắc chắn phải nhận một kết quả sai nếu chạy các test của mình theo một thứ tự xác định, hoặc nếu chúng chỉ làm việc khi cơ sở dữ liệu hoặc kết nối mạng của bạn đang hoạt động.

(Bằng cách này, đôi khi kiến trúc của bạn có thể có nghĩa là code của bạn tương tác với các biến static trong quá trình unit test. Hãy tránh điều này nếu có thể, nhưng nếu bạn không thể, ít nhất là đảm bảo mỗi test sẽ reset các biến static liên quan đến một trạng thái được biết đến trước khi nó chạy.)

Tránh những điều kiện tiên quyết không cần thiết Tránh việc có các code setup chung chạy vào lúc bắt đầu của rất nhiều các test không liên quan. Nếu không, nó không rõ ràng về những giả định mà mỗi test dựa trên đó, và chỉ ra rằng bạn đang không test chỉ một đơn vị duy nhất.

Một ngoại lệ: Đôi khi tôi thấy nó hữu ích khi có một phương pháp thiết lập chung được chia sẻ bởi một số lượng rất nhỏ các unit test (một nhóm nhỏ) nhưng chỉ khi tất cả những test đó yêu cầu tất cả những điều kiện tiên quyết này. Nó liên quan đến một unit testing patterncontext-specification, nhưng vẫn có rủi ro là nó trở nên không thể bảo trì được nếu bạn cố gắng sử dụng lại cùng một setup code cho một số lượng lớn các test.

  • Đừng sử dụng unit test trong phần thiết lập cấu hình

Theo định nghĩa, các thiết lập cấu hình của bạn không phải là một phần của bất kỳ unit code nào (đó là lý do tại sao bạn trích xuất các thiết lập ra khỏi code của unit của bạn). Ngay cả khi bạn có thể viết một unit test để kiểm tra cấu hình của mình, nó chỉ đơn thuần buộc bạn phải xác định cấu hình tương tự tại một vị trí dự phòng bổ sung. Xin chúc mừng: nó chứng tỏ rằng bạn có thể copy và paste!

  • Đặt tên các unit test của bạn một cách rõ ràng và nhất quán

Nếu bạn đang kiểm thử action Purchase của ProductController hành động như thế nào khi hết hàng trong kho (stock is zero), sau đó có thể có một lớp test fixture được gọi là PurchasingTests cùng với một unit test gọi là ProductPurchaseAction_IfStockIsZero_RendersOutOfStockView(). Cái tên này mô tả đối tượng (action Purchase của ProductController), kịch bản (hết hàng trong kho), và kết quả (hiển thị trên view là "hết hàng").

Tôi không biết liệu có một cái tên cho mô hình đặt tên này hay không, mặc dù tôi biết nhiều người khác cũng làm theo cách này. Đây chỉ là 1 ví dụ cho cách đặt tên của unit test của bạn, bạn có thể dùng cách khác miễn sao khi chúng ta đọc qua tên của nó là có thể hiểu được mục đích của unit test đó là gì.

Tránh sử dụng những tên unit test không rõ nghĩa như Purchase() hoặc OutOfStock(). Việc bảo trì sẽ rất khó khăn nếu bạn không biết là mình đang cố duy trì cái gì.

3. Kết luận

Không còn nghi ngờ gì nữa, unit testing có thể làm tăng đáng kể chất lượng dự án của bạn. Nhiều người trong ngành cho rằng việc có thêm bất kỳ unit test thì tốt hơn là không có gì, nhưng tôi không đồng tình với quan điểm đó: một bộ test có thể là một tài sản tuyệt vời, hoặc nó có thể là một gánh nặng rất lớn mà không mang lại giá trị gì nhiều. Nó phụ thuộc vào chất lượng của những test này, mà dường như được xác định bởi mức độ các nhà phát triển hiểu rõ những mục tiêu và nguyên tắc của unit testing.

Recommends

Trên viblo cũng có một vài bài viết viết về cách viết unit test như thế nào cho đúng, mọi người đọc tham khảo nhé


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í