The Story

Mock tạo thủ công có thể dùng để test trong Golang

Xin chào các bạn.

Tôi là Yosuke, hiện tôi là một kỹ sư Golang đang làm việc tại chi nhánh Kyoto của Money Forward.

Tôi là một thành viên trong team KTA (Kyoto Development Division Technical Architect Team), chúng tôi chuyên sử dụng Golang thiết kế và thực thi các tính năng nhằm tăng performance và sử dụng chung cho các product.

Các bạn có bao giờ viết test bằng Golang chưa?

Riêng chúng tôi cố gắng viết test hằng ngày, vì tin rằng nó sẽ giúp cho việc tăng tốc độ phát triển dự án về lâu về dài.

Những lúc dùng  Golang, team KTA thường dựa vào Clean Architecture để phân chia các package, hay định nghĩa các interface… Nhưng cũng chính vì vậy mà việc viết test cũng khó hơn.  

Do đó, tôi sẽ giới thiệu với các bạn về kỹ thuật tạo Mock để có thể dùng cho các trường hợp tương tự như trên. 

Thường chúng ta nghĩ nên dùng Clean Architecture thì test dễ hơn…. 

Khi chúng tôi đang tập tành áp dụng ý tưởng Clean Architecture thì dựa theo nguyên lý DIP: Dependency Inversion Principle (nguyên lý Dependency Inversion), và implement business logic layer dựa trên các interface có sẵn. 

Sau đó, ta có được một phương thức implement mang tính loose coupling (liên kết không phụ thuộc) với nhau, bằng cách kết nối với Data Access layer thông qua interface (loose coupling).

Vì đây là một dạng loose coupling, ví dụ như chúng ta muốn sửa code để cải thiện performance của Data Access layer, thì cũng không phát sinh Code Fix tại business logic layer, và đồng thời được phân chia ra một cách riêng biệt nên có thể test một cách độc lập, lượng code thực thi càng lớn thì lợi ích từ cách này càng nhiều. 

Nhưng khi nghĩ tới việc viết test như thế nào, thì đa phần chúng ta hay nghĩ tới việc dùng mock.

Để test trên business layer, nếu chúng ta không dùng mock mà tích hợp layer access với việc instant hoá thì sẽ xảy ra trường hợp chúng ta sẽ không phân biệt được nó đang test business hay đang test data access.

Tôi sẽ giới thiệu với các bạn làm thế nào để vừa để viết test cũng như cách viết mock dựa trên các dòng code điển hình hay xuất hiện. 

Code của đối tượng test

Thông thường, định nghĩa của interface sẽ như ví dụ sau:

Lần lượt định nghĩa từng data access của User và UserGroup.

Dù là chỉ đơn giản là định nghĩa CRUD(Create, Read, Update, Delete) thường hay xuất hiện trong ứng dụng thôi, mà lợi ích của nó mang lại thường rất nhiều.

Mình sẽ dựa trên model để thực hiện việc định nghĩa này, và dưới đây là code của nó:

Và dưới đây là business layer đã được implement từ dòng code trên. Hơi dài nhưng đầy đủ để các bạn có thể cảm nhận một cách thực tế nhất.

Thực tế đoạn code này chạy được, Nhưng phải lưu ý chuẩn bị trước data access layer, nhưng do đây là test nên tôi đã giản lược phần đó. 

Thực thi test

Giờ chúng ta sẽ test thử hàm User.Create() trong package service.

Mock này thật sự có thể được tạo thủ công một cách đơn giản.*

Khi tạo mock của interface, đúng ra là phải tạo sẵn đối tượng đã thực thi tất cả method, nhưng ở đây tôi chỉ giới thiệu 3 method thực tế có trong Create thôi :(domain.User.Create,domain.UserGroup.Create, domain.User Group.AddUser)

Ngoài ra còn có 8 method khác nhưng do không liên quan lắm nên tôi không đưa vào đây.

Với các trường hợp như vậy chúng ta có thể code như sau: 

Chắc hẳn là các bạn đang nghĩ: [ủa sao không thấy hiện thực method nào hết trơn vậy?]

Nhưng qua việc hiện thực method này thì có thể chạy được hàm go test  một cách suôn sẻ!

Tại sao compiler trong Golang lại cho phép bạn thực hiện được điều này, là vì các interface được nhúng vào từng struct khác nhau.

Interface được nhúng vô struct sẽ trở thành 1 trường chưa có giá trị (nil value), interface này chứa tất cả các định nghĩa cần thiết cho method, bao gồm các định nghĩa cần thiết cho cả struct.

Tuy nhiên, do giá trị của trường này là dạng dữ liệu nil, thật tế là khi mình dùng mock, thì ngay lúc hàm userRepoMock.Create() được gọi ra ….

Thì hàm sẽ không chạy được vì xuất hiện lỗi như trên,

Để cho hàm chạy được thì ít nhất nó phải nó cần phải chứa một thực thể nào đó.

Kỹ thuật mà tôi muốn đề cập nhất trong bài viết này chính là  thực thi sau: 

Thêm Prefix ( Folder) Fake* vào các hàm của struct để thấy được ưu điểm của nó. Sau đó, ta chỉ cần gọi nội dung của hàm Fake* ra là được.

Do signature của phương thức này mà khác với định nghĩa trên interface thì không được, nên tôi sử dụng copy paste.

Sau khi tạo rồi dùng mock như hướng dẫn trên, test code sẽ được viết như sau: 

Giống như là mình đang viết trên TableDrivenTests vậy.

Điểm hay của việc tạo mock bằng phương pháp này chính là việc bạn có thể định nghĩa một tên Fake* có thể thực hiện các tác vụ được yêu cầu theo từng test case.

Khi bạn muốn hàm mock chạy một cách thông thường, thì bạn chỉ cần tạo một hàm trả về các giá trị thông thường rồi sau đó ráp nó vào, còn khi muốn hàm chạy theo cách không thông thường thì chỉ cần return error trong hàm.

Nếu bạn muốn bảo đảm là nó đã chắc chắn được gọi, bạn có thể dùng vài cái counter, sau đó update từ hàm mà bạn đã gắn vô Fake*

Việc nó được sử dụng một cách dễ dàng cho phép nó có thể test được các business logic mà thực tế thường rất phức tạp, nên giờ các ca phức tạp đó cũng trở nên dễ dàng được xử lý hơn. 

So sánh với mock library

Nhắc tới Mock Library thì thường người ta hay liên tưởng tới việc sử dụng 2 hàm gomock , testify/mock có trong mockery.

Mock code sẽ được tạo tự động dựa vào góc interface.

Giờ thì mình thử so sánh test code được sử dụng với mock được tạo ra khi dùng gomock thử nhe. 

Mock được tự động tạo được kết hợp với package name có thể tham chiếu từ test code. (Set package thành dạng package *_test mà không bao gồm build)

Nhìn bên ngoài của testcode được tạo bằng mock thì nó rất giống với gomock được tạo thủ công.

Tuy nhiên, do gomock được thư viện hoá, nên nó được cung cấp các chức năng với các ưu nhược điểm như sau.

  • Ưu điểm
    • Mock Code được tạo một cách tự động
    • Số lần được gọi bởi method và assertion trong tuần tự.
    • Assertion trong giá trị input của method
  • Khuyết điểm
    • Hình thức của code khi dùng mock là kiểu: interface{}  (  EXPECT(). và  DoAndReturn)

Trường hợp tạo thủ công thì các đặt tính lại ngược hẳn

  • Ưu điểm
    • Code có sử dụng mock rất dễ đọc
    • Nếu có các behavior dùng chung hay mặc định thì có thể kết hợp chúng để sử dụng mọi nơi.
    • Có thể chỉnh sửa được nếu có yêu cầu đặc thù nào đó trên mock
  • Nhược điểm
    • Vì phải làm thủ công nên có thể phát sinh bug khi viết mock có sự sai sót

gomock có ưu điểm là có thể tạo tự động được, và behavior của mock là thiết lập từng method một trên đơn bị test code nên khó có thể dùng chung behavior của mock, với đặc tính như vậy nên tôi không nghĩ là nó hợp với việc dùng làm usecase.

Ứng dụng của mock được tạo thủ công

Chính vì đây là manual mock nên có thể giới thiệu một cách đơn giản.

Khi tiến hành áp dụng Clean Architecture có sử dụng interface thì các hằng số phát sinh ở các layer bên ngoài đa phần cần phải xử lý các mối quan hệ lệ thuộc, để giải quyết các vấn đề về mối quan hệ lệ thuộc thì chúng ta hoặc thực thi registry, hoặc cân nhắc việc dùng tool có DI trong wire chẳng hạn.

Khi thực thi registry trong KTA thì code sẽ như sau: 

Code này đã được giản lược lại rồi, các mối ràng buộc này cụ thể là các ràng buộc giữa các service ngoài của client, DB connection và giữa các server với nhau.

Việc khởi tạo hàm service.NewUser()  cũng rất đơn giản như sau. 

Nếu vậy thì cần phải truyền mock của registry vào, nhưng việc này có thể giải quyết thay bằng cách chỉ cần thêm 1 tí code vào hàm mock. 

Khi dùng hàm này, thì việc viết test có thay đổi 1 chút. 

Khi viết bằng TableDrivenTests thì có thể thay đổi mà không xảy ra conflict nào.

Code được viết bằng TableDrivenTests thì nó linh hoạt hơn.

Tương tự như khi dùng gomock, cũng không phải là không thể dùng được nhưng thời gian tạo ra còn tốn hơn là tạo code trong mock bằng thủ công. 

Tổng kết 

Trong Golang, người ta hay dùng ý tưởng Clean Architecture hay với mục đích gần như vậy, ví dụ như sử dụng nó để tăng khả năng test, duy trì độ dễ đọc của code trong interface chẳng hạn, và các interface kiểu như io.Reader thì được trường tượng hoá và sử dụng khắp nơi.

Với trường hợp như trên, tôi khuyến khích các bạn sử dụng mock để dùng cho việc test.

Nếu bạn cũng muốn dùng code của mình một cách hiệu quả thì hãy thử phương pháp này nhé.  

Lời cuối

Năm 2022 MFV vẫn tích cực chiêu mộ nhiều kỹ sư mới.

Nếu bạn muốn thách thức với các loại kỹ thuật mới, mang giá trị đến cho users thông qua cái kỹ thuật này,bạn sẽ chọn MFV chứ.

Mong có sự ứng tuyển của các bạn!

--------

*: Sự phân loại trong nó cũng gần với Test Stub và Fake Object vậy. 

Translator: Michael