kiến thức Continous Integration - P2 - TDD

  • Người tạo chủ đề Người tạo chủ đề hoctrokha
  • Ngày bắt đầu Ngày bắt đầu
Trạng thái
Không mở để trả lời thêm.

hoctrokha

Senior Member
Tuần qua mình bận quá nên cho dù đã viết xong mà cũng không dám up bài, sợ k có thời gian để thảo luận với mọi người.
Ok, vào vấn đề nhé.

Link P1: https://voz.vn/t/continous-integration-p1-what-is-it.497164/

CV có làm CI mà chưa viết test bao giờ
Đừng bao giờ nói với tôi rằng bạn đang làm CI trong khi bạn không viết test.
Xóa ngay dòng CI ra khỏi CV hộ tôi nếu bạn không có test trong dự án.

Test là điều bắt buộc để bạn có thể commit code trực tiếp lên branch main. Ai cũng hiểu độ quan trọng của nó như thế nào. Nhưng "làm sao để viết test xịn, giúp catch được thật nhiều bug" là một câu hỏi cần nhiều kinh nghiệm để trả lời.

TDD
Khi nhắc đến test, TDD là một thuật ngữ thường được nhắc đến, idea của nó là thế này:
Thường các bạn viết code trước xong viết test, khi làm với TDD thì các bạn viết test trước, sau đó mới viết code. Bạn sẽ tập trung vào test chứ k còn tập trung vào code nhiều nữa.

Theo lý thuyết thì khi dùng TDD:
  • Code của bạn sẽ có ít bug hơn trên production
  • Khi cần refactor code, chỉ cần đảm bảo test không bị break thì mọi thứ vẫn ổn

Nếu bạn search google thì sẽ ra thêm 1 đống benefit nữa nhưng mình k tin, mình chỉ thấy được benefit ở 2 thằng này thôi.

Tuy nhiên đời không như mơ, phần lớn các project thử apply TDD đều fail một cách ngoạn mục (theo mình nhìn thấy và nói chuyện với các dev khác). Khi nói chuyện với các dev khác, có người lớn hơn mình, có người nhỏ hơn mình, người mới làm TDD được vài tháng, người thì cả chục project viết đủ loại test: black box, white box, unit, integration ... , nhưng ai cũng có một suy nghĩ: TDD là trò hề, chỉ phí thời gian.

Các lý do từ phía dự án là:
  • Chi phí để bắt đầu cao (training, refactor code để có thể dependency injection ...)
  • Thời gian release 1 feature lâu hơn (20 - 50%)
  • TDD gần như bất khả thi trên các project lâu năm (cái này không đúng, nhưng rất nhiều người vẫn tin, mình sẽ giải thích sau)
  • Ưu tiên MVP hoặc không biết assumtion của mình về thị trường có đúng hay ko, lỡ mà sai thì test coi như vứt hết.

Các lý do đó không sai, nhưng thậm chí khi đã vượt qua được các câu hỏi đó, ta lại có các câu hỏi từ phía dev:
  • Bao nhiêu test là đủ?
  • Mỗi khi refactor code thì hàng loạt test bị fail, cả ngày chỉ dành để fix test chứ chả làm được gì.
  • Tại sao đã có 100% test coverage mà vẫn có bug trên production?

Hãy đặt những câu hỏi đúng
Với mình, test được xem là tốt khi:
  • Nhanh, ko bị flaky
  • Mô tả đúng cái hệ thống hứa hẹn
  • Không break khi refactor

Chúng ta phân biệt ra nhiều loại test khác nhau, Unit, Integration, e2e ... nhưng mình nghĩ các bạn ko nên quan tâm đến các khái niệm đó.
Thực chất Integration test chỉ là unit test, nhưng mình integrate thêm nhiều thứ hơn. e2e test chính là integration test, nhưng mình có integrate thêm cái browser, k8s, db ...
Khi bạn integrate càng nhiều thứ "thật" vào trong 1 test case thì test càng chậm, nhưng độ tin cậy của test càng cao.
Đừng bao giờ hỏi nhiêu đây Unit test đã đủ chưa, nhiêu đây Integration test đã đủ chưa, có cần viết e2e hay ko ...
Đáng ra bạn phải hỏi:
  • Mình muốn test đó đảm bảo cho mình điều gì?
  • Điều mình muốn đảm bảo đó có phải là requirement không, nếu ko phải requirement thì test này có thực sự cần thiết?
  • Test đó phải phản hồi nhanh cỡ nào?
  • Để đạt được sự đảm bảo đó, tốc độ phản hồi đó thì mình nên test ở thằng nào và dùng tool gì để viết test là hợp lý?
Hãy tự đặt và trả lời các câu hỏi đó trước khi viết test.
Khi trả lời các câu hỏi đó đúng, số lượng test case mà bạn cần viết chắc chắn sẽ giảm và test của bạn sẽ cứng cáp hơn trong các lần refactor.


Lời Khuyên
Mỗi framework/ngôn ngữ sẽ có một đặc điểm riêng, nên việc đưa ra 1 usecase nào đó quá cụ thể thì người này hiểu, chưa chắc người khác đã hiểu. Bởi vậy trước hết mình sẽ đưa ra một số lời khuyên chung:

Lời khuyên 1: Xác định rõ cần test cái gì, test ở level nào.
Ví dụ bạn có một api controller, controller này gọi đến một hàm ở một file khác để lấy data, sau đó nó chỉnh sửa j đó trên data rồi trả về cho client. Bạn sẽ test ở đâu, test cái controller hay cái hàm kia, hay là test ở cả 2 chỗ?
Kinh nghiệm của mình là integrate nhiều thành phần vào nhất có thể nhưng vẫn phải đảm bảo được:
- test chạy nhanh
- test không bị flaky, chạy 10 lần phải cùng ra 10 kết quả, các test không phụ thuộc vào thứ tự chạy và không ảnh hưởng đến nhau.
Trong trường hợp trên thường thì mình sẽ dựng nguyên cả cái app lên để test từ thằng middleware xuống thằng controller thông qua http. Tuy nhiên tùy framework bạn đang dùng có hỗ trợ viết test kiểu đó hay ko nhé, test qua http ko có nghĩa là e2e đâu.

Lời khuyên 2: Viết test trước khi viết code
Điều này đi ngược lại với TDD một cách rõ ràng, nhưng thực tế rất rất ít team viết test trước khi viết code.
Vậy hãy viết test trước khi viết code, nó sẽ giải quyết được một đống vấn đề của các bạn.

Lời khuyên 3: Không tưởng tượng ra code "tương lai" trong khi viết test
Mặc dù cố gắng viết test trước khi viết code, nhưng các bạn thường tưởng tượng ra mình sẽ viết code như thế nào, sau đó viết test dựa theo đống code tưởng tượng đó. Nghe có vẻ hài hước nhưng rất rất nhiều dev bị sa vào bẫy này.
Muốn giải quyết vấn đề này thì đọc lời khuyên thứ 4.

Lời khuyên 4: Đừng bao giờ test hệ thống làm như thế nào, hãy test xem hệ thống làm cái gì.
Test của bạn không được chứa bất cứ thứ gì liên quan đến implement detail. Mặc dù một số case không thể tránh khỏi, nhưng hãy cố gắng không để test của bạn biết code làm như thế nào, dùng lib gì ...
Nếu như testing framework mà bạn đang dùng bắt bạn phải biết về implement detail thì bạn nên kiếm một thằng khác tốt hơn.
Hãy test các side effect của code như: test kết quả trả về, test UI hiển thị những gì, test dữ liệu dưới DB có thay đổi đúng như mình mong đợi không ...
Vì vậy, hãy nhìn vào requirement trong ticket và xem xem tính năng của mình cần đáp ứng những gì, qua đó viết các test case.
Bên cạnh đó, hãy viết test theo góc nhìn của consumer. Khi bạn làm UI thì consumer là người dùng, khi bạn làm API thì consumer là UI hoặc các dev ở các service khác.
Riêng thằng này các bạn có thể search BDD để tìm hiểu thêm nhé.

Lời khuyên 5: Đừng mock code của bạn, mock càng nhiều, độ tin cậy càng giảm.
Thay vì mock axios, hãy dùng msw để mock server. Thay vì mock cái ORM, hãy dùng in-memory db để mock db.
Nên nhớ, lib chính là một phần code trong hệ thống mà bạn đang test, khác biệt ở chỗ nó được viết bởi một người không thuộc dự án. Mock càng nhiều, khả năng code mock sẽ chạy không giống như code thật chạy, dẫn đến bug.
Điều tương tự cũng đúng khi bạn cố dependency injection vào để test một unit nào đó trong sự cô lập tuyệt đối, điều đó không tốt. Hãy test code khi chúng làm việc với nhau như trên production, cố gắng integrate càng nhiều thành phần trong code của bạn vào test nhất có thể.


Ngoài các lời khuyên chung, mình cũng chia sẻ một số lời khuyên cụ thể hơn:
1, Chỉ dùng container nếu bắt buộc.
Việc dùng container để tạo các "edge" như db, message broker ... với mình hoàn toàn không phù hợp cho TDD bởi:
  • Nó chậm, mình cần thấy kết quả test nhanh
  • Để giúp test chạy nhanh hơn, các dev thường dùng 1 container cho nhiều test cùng 1 lúc, điều này có thể tạo ra flaky test.
Quyết định dùng container hay ko là điều khá nhạy cảm, bởi nếu lib dùng để tương tác với "edge" có behavior đơn giản, bạn có thể có lựa chọn khác là mock cái lib đó. Tất nhiên điều đó sẽ khiến độ tin cậy của test giảm xuống, nhưng đổi lại test sẽ chạy nhanh hơn.
Dù quyết định của bạn như thế nào đi nữa, hãy luôn nhớ rằng: nhắm mắt nhắm mũi dùng container ko phải là chuyện tốt.
2, Dựng sequential diagram trước khi viết test nếu có thể
Sequential diagram rất hữu ích để xác định các test case, và cũng rất hữu ích cho QE, IT/Devops team ... hãy dựng sequential diagram nếu bạn có thể nhé.
3, Tránh dùng các hook trong jest kiểu như beforeEach/All, afterEach/All
https://kentcdodds.com/blog/avoid-nesting-when-youre-testing


Pattern nhiều để làm gì?
Qua những lời khuyên trên, các bạn có thể thấy mình không khuyến khích mock hoặc DI dưới mọi hình thức.
Bởi vậy, code của bạn dẫu không có DI, bẩn bựa cỡ nào, nhưng vẫn hoàn toàn có thể test được nếu như test đúng nơi, test đúng cái cần test.
Từ những kinh nghiệm đó khiến mình có suy nghĩ: đừng quá quan trọng code structure/pattern ... khi review mình không quan tâm những thứ đó.
Cái mình quan tâm là test của bạn liệu có cho mình sự tin tưởng rằng sản phẩm sẽ chạy đúng như mong đợi hay không,
nếu sản phẩm chạy đúng như mong đợi thì tại sao ta lại cần quan tâm tới pattern này nọ làm gì?
Thử nghĩ xem, mục đích tối thượng của việc code chính là tạo ra sản phẩm giống như requirement, nếu nó thực sự chạy giống như requirement rồi thì nó có code kiểu hầm bà là nhằng, tên biến đặt kiểu 1 ký tự thì sao phải quan tâm?
Tất nhiên áp dụng pattern, convention này nọ thì sẽ thấy nghệ thuật hơn, các bạn biết pattern rồi áp dụng vào càng tốt, nhưng thực chất framework chúng ta đang dùng cũng đã có một đống best practice ở trong đó rồi. Kể cả code có tệ cỡ nào mà có test xịn thì đọc vẫn thoải mái. Bởi vậy, khi áp dụng TDD thì bạn hãy thả lỏng về mặt convention/pattern ra một xíu, nếu code xấu quá k chấp nhận đc thì mình refactor sau, có test rồi nên đừng nặng nề quá.
Nói chung là bóp một bên thôi, bóp 2 bên 1 lúc k thở được.

Vậy test coverage có quan trọng không?
Quan trọng chứ. Tuy nó không nói lên test có xịn hay không, nhưng nó cho thấy những đoạn code nào bạn lỡ viết ra nhưng dư thừa hoặc chưa có test.
Tuy nhiên, có những file bạn không thể nào viết test cho nó được, hoặc vẫn viết được nhưng bạn cảm thấy nó không có nhiều ý nghĩa, ví dụ như file bootstrap app lên vậy, bạn expect sau khi chạy xong module đó thì app sẽ lắng nghe trên port 3000 chẳng hạn.
Sẽ đơn giản và tin cậy hơn nhiều nếu như mình dùng e2e test cho case đó.
Bởi vậy mình thường dùng test coverage như một trợ lý để tham khảo thôi, chứ không bao giờ đặt nặng nó.

Bước tiếp theo
Ok, nếu team bạn tuyển được một người đủ trình để viết test xịn thì còn gì phải lo, nhưng nếu team bạn giống như bao team khác: 2 senior và 3 fresher thì sao? Hoặc thậm chí cả team k có ai biết viết test nhưng vẫn muốn làm CI thì sao?
  • Làm sao để kiểm chứng chất lượng của các test khi không có PR?
  • Kể cả cẩn thận, không có gì đảm bảo commit sẽ ko bị fail khi chạy trên CI hoặc xảy ra vấn đề khi chạy trên máy của dev khác, lúc đó phải làm sao?
Trong bài tiếp theo mình sẽ giúp các bạn trả lời câu hỏi đó.
Đây là hai vấn đề khó khăn nhất khi không có PR và cách giải quyết của mình hy vọng sẽ khiến bạn bất ngờ.

Các câu hỏi từ P1:
Sau khi đăng P1, mình thấy các bạn có thảo luận với nhau và có một số câu hỏi cần được trả lời:
- Test cho các codebase lớn, gồm rất rất nhiều test: sẽ giải thích ở một bài nào đó nếu có thể, tại cái đó ko phải trọng tâm vào lúc này, bạn có thể search nx monorepo nếu muốn xem trước. Cơ bản mình thấy thằng này cũng đã đủ rồi, nếu bạn đã biết rồi và cần nhiều hơn/biết nhiều hơn thì có thể chia sẻ.
- Tại sao lại cần release branch mà không release trực tiếp từ main: Mình sẽ tìm một thời điểm phù hợp để trả lời câu hỏi này, nếu được thì mình sẽ cho vào phần tiếp theo, nếu ko hợp thì để sau vậy.
 
Sửa lần cuối:
Bạn đẩy vào 1 thread của bạn cho mọi ng dễ theo dõi. Chỉ nên tách ra khi là chủ đề khác.
Nếu nhiều bài cùng 1 topic thì ko nên tách thớt.
 
Trạng thái
Không mở để trả lời thêm.

Thống kê chủ đề

Ngày tạo
hoctrokha,
Người trả lời cuối
Fire Of Heart,
Trả lời
1
Lượt xem
1.980
Quay lại
Lên đầu trang