Unit Test là thứ rất ít bạn sinh viên để ý đến. Nên lúc được
yêu cầu viết test cho hàm thì các bạn mới bắt đầu tìm hiểu unit test là gì? viết
test như nào?… Google trả về cho bạn kết quả kiểu như a + b = c thì thay a = 1
và b = 1 chạy hàm ra c = 2 là đúng. Thật khó chịu khi các ví dụ rất chung chung
chả thể nào áp dụng vào dự án của bạn. Đừng lo bài viết này sinh ra để làm bạn
thỏa mãn ít nhất nó sẽ giúp bạn biết bạn cần những gì để bắt đầu viết unit
test.
1. CHUẨN BỊ
Mình có chuẩn bị 1 projetc .NET API với kiến trúc mô hình 3
lớp (3-layer) siêu kinh điển, mình đã gom lại hết tất cả vào 1 project
nhưng vẫn đảm bảo đầy đủ các thành phần cần thiết bao gồm: (Hình 1)
- Controller : Nhận request và trả response cho client
- Data : chứa các entity và DbContext
- Middlewares : thực hiện các nhiệm vụ như xác thực, xử lý lỗi…
- Models : Các đối tượng được sử dụng để chuyển dữ liệu giữa các tầng hoặc được hiểu như DTOs (Data Transfer Objects)
- Repository : Nhiệm vụ truy vấn csdl
- Service : Xử lý tất cả các logic nghiệp vụ
Tạo project test:
- New 1
projetc bằng xUnit Test Projetc (hình 2)
- Tạo
các folder (hình 3)
- Install
2 thư viện Moq và FluentAssertions (Moq là đặc biệt cần nha)
Github: https://github.com/viethung23/unit-test-demo
2. THỰC HIỆN
Mình sẽ thực hiện Viết Test cho 3 tầng Controller, Service ,
Repository và trước tiên các bạn cần biết 1 số khái niệm liên quan:
- Arrange - Chuẩn bị dữ liệu đầu vào và các điều kiện khác để thực thi test case.
- Act - bước thực hiện gọi hàm và nhận về kết quả thực tế
- Assert - bước so sánh kết quả thực tế với kết quả mong đợi .
*Lưu ý : có thể tùy theo mô hình phát triển của bạn chọn là
gì để thực hiện test trước hoặc sau khi code.
Repository (Hình 5):
Class ProductRepositoryTests sẽ chứa các test cho tất cả các
hàm của ProductRepository. Đầu tiên bạn sẽ phải khởi tạo đối tượng là class chứa
các hàm bạn muốn test. Và vì khi khởi tạo ProductRepository() yêu cầu truyền
vào TestDbContext nên cũng cần phải khởi tạo mới 1 thằng TestDbContext để truyền,
với mục đích là test nên context sẽ được khởi tạo với cấu hình dùng
InMemoryDatabase để cô lập với môi trường khác (line 15 -19).
Hãy chuẩn bị các test case(kịch bản kiểm thử) cho từng function và đặt tên hàm
test của bạn thật rõ ràng cho các case. Trong hình 5 mình có 1 test case
là AddAsync_AddProductCorrectly(), nghĩa trên mặt chữ luôn “tôi
đang test hàm AddAsync với trường hợp data truyền vào đúng thì hàm AddAsync phải
thêm được product vào Db”. Ở trong tôi có comment các phần cho dễ nhìn.
- Arrange chuẩn bị dữ liệu tôi tạo 1 product .
- Act tôi thực hiện gọi hàm AddAsync() và truyền cái product đã chuẩn bị từ Arrange sau đó lưu vào DB.
- Assert tôi kì vọng result trả ra 1 có nghĩa là 1 product đã được add thành công vào DB.
Bạn có thể có nhiều test case khác nữa cho hàm này để cover được nhiều tình huống có thể xảy ra ví dụ "AddAsync_ThrowsArgumentNullException_WhenProductIsNull()" là product = null khi truyền vào hàm AddAsync() và còn nhiều trường hợp khác nữa.
- Act tôi thực hiện gọi hàm AddAsync() và truyền cái product đã chuẩn bị từ Arrange sau đó lưu vào DB.
- Assert tôi kì vọng result trả ra 1 có nghĩa là 1 product đã được add thành công vào DB.
Bạn có thể có nhiều test case khác nữa cho hàm này để cover được nhiều tình huống có thể xảy ra ví dụ "AddAsync_ThrowsArgumentNullException_WhenProductIsNull()" là product = null khi truyền vào hàm AddAsync() và còn nhiều trường hợp khác nữa.
Service (hình 6)
Đây là tầng xử lý logic do đó nó có thể inject rất nhiều các dịch vụ khác nhau. rồi mỗi cái dịch vụ đó lại có các phụ thuộc khác :}} Nghĩ cảnh khởi tạo đối tượng thôi là toát mồ hồi nhưng việc gì khó đã có Moq lo. Moq là một thư viện mô phỏng cho phép các nhà phát triển tạo ra các đối tượng giả (mock objects), đây là những đối tượng có hành vi có thể được cấu hình sẵn để mô phỏng các tương tác trong một môi trường kiểm thử mà không cần phụ thuộc vào những thành phần bên ngoài nào khác.
Nhìn vào hình 6, ProductServiceTests cần khởi tạo các đối tượng mock Của
repo và mapper sau đó mới thực hiện khởi tạo ProductService (line 18 -20). Bạn
tiêm bao nhiêu phụ thuộc thì mock bấy nhiêu đối tượng là xong, quá dễ luôn!!
Qua bên Hàm test thì cũng cần setup các data và setup các hành vi cho các phụ
thuộc. Ví dụ :
[Fact]
public async Task CreateProduct_ReturnsProductResponse_WhenSaveSuccessful()
{
// Arrange
var request = new CreateProductRequest ( "New Product","test", 100 );
var product = new Product { Id = 1, Name = "New Product", Price = 100 };
var response = new ProductResponse ( 1, "New Product", "test", 100 );
_mockMapper.Setup(m => m.Map<Product>(It.IsAny<CreateProductRequest>()))
.Returns(product);
_mockMapper.Setup(m => m.Map<ProductResponse>(It.IsAny<CreateProductRequest>()))
.Returns(response);
_mockRepo.Setup(r => r.AddAsync(It.IsAny<Product>()))
.Returns(Task.CompletedTask);
_mockRepo.Setup(r => r.SaveChangeAsync())
.ReturnsAsync(1);
// Act
var result = await _service.CreateProduct(request);
// Assert
Assert.NotNull(result);
Assert.Equal(response.Name, result.Name);
_mockRepo.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Once);
_mockRepo.Verify(r => r.SaveChangeAsync(), Times.Once);
}
Arrange:
- request:
Đây là đối tượng yêu cầu để tạo sản phẩm mới.
- product:
Đối tượng sản phẩm được dùng để mô phỏng kết quả của việc ánh xạ từ request.
- response:
Đối tượng phản hồi mong đợi khi sản phẩm được tạo thành công.
- _mockMapper:
Cấu hình giả lập cho việc ánh xạ từ CreateProductRequest sang Product,
và từ Product sang ProductResponse.
- _mockRepo: Cấu hình giả lập cho repository để mô phỏng thêm sản phẩm vào cơ sở dữ liệu và lưu các thay đổi.
Act:
- var result = await _service.CreateProduct(request): Thực hiện gọi phương thức và lưu kết quả trả về.
Assert:
- Assert.NotNull(result):
Kiểm tra xem kết quả trả về có khác null không.
- Assert.Equal(response.Name,
result.Name): So sánh tên sản phẩm trong phản hồi mong đợi và kết quả
trả về.
- _mockRepo.Verify(r
=> r.AddAsync(It.IsAny<Product>()), Times.Once): Kiểm tra xem
phương thức AddAsync có được gọi đúng một lần không.
- _mockRepo.Verify(r
=> r.SaveChangeAsync(), Times.Once): Kiểm tra xem phương thức SaveChangeAsync có
được gọi đúng một lần không.
Controller (Hình 7):
Ở tầng Controller này tương tự như service cũng sẽ qua các
bước Arrange, Act, Assert. Bạn cần tìm hiểu thêm về các tính năng khác của Moq
để hỉu hơn nữa.
3. TỔNG KẾT
Đến đây mình hy vọng các bạn đã có cái nhìn tổng quan về
cách thực hiện test cho dự án sử dụng .NET và nắm được những thư viện cần thiết.
Cảm ơn các bạn đã quan tâm và hy vọng bài viết sẽ mang lại giá trị cho mọi người.
Hẹn gặp lại ở các bài viết sau!




