Fake thay phụ thuộc
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
OrderService kiểm tra tồn kho rồi trừ kho qua IProductStore. Test nó
với DbProductStore thật thì phải bật Oracle và chuẩn bị dữ liệu, test chạy
chậm và lúc xanh lúc đỏ tuỳ dữ liệu trong database. Thay IProductStore bằng
một bản giả trong bộ nhớ thì test chạy trong vài mili giây, lần nào cũng cho
cùng kết quả.
Khái niệm
♻️ Fake: class tự viết implement cùng interface với phụ thuộc thật, giữ dữ liệu trong bộ nhớ để test dùng thay cho database hay API.
FakeProductStore ở bài Tách giao diện và dữ liệu của khoá WinForms là một
fake. Bài này viết lại nó, thêm bộ đếm số lần ghi.
Như bài DIP và dependency injection, OrderService nhận interface
IProductStore qua constructor, nên truyền fake hay bản thật đều được.
Ví dụ
Class cần test, nằm trong project ShopApi. IProductStore và Product
giữ đúng như bài Truy vấn với EF Core:
public class OrderService
{
private readonly IProductStore _store;
public OrderService(IProductStore store)
{
_store = store;
}
public async Task<bool> PlaceAsync(
int productId, int quantity)
{
List<Product> inStock =
await _store.InStockAsync();
Product? product =
inStock.Find(p => p.Id == productId);
if (product == null || product.Stock < quantity)
{
return false;
}
await _store.SetStockAsync(
productId, product.Stock - quantity);
return true;
}
}
// Có sẵn trong ShopApi từ khoá ASP.NET Core
public interface IProductStore
{
Task<List<Product>> InStockAsync();
Task<bool> SetStockAsync(int id, int stock);
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int Stock { get; set; }
}Fake và test, nằm trong ShopApi.Tests:
using Xunit;
public class FakeProductStore : IProductStore
{
public List<Product> Products { get; } =
new List<Product>();
public int SetStockCalls { get; private set; }
public Task<List<Product>> InStockAsync()
{
var result = Products.FindAll(p => p.Stock > 0);
return Task.FromResult(result);
}
public Task<bool> SetStockAsync(int id, int stock)
{
SetStockCalls++;
Product? p = Products.Find(x => x.Id == id);
if (p == null)
{
return Task.FromResult(false);
}
p.Stock = stock;
return Task.FromResult(true);
}
}
public class OrderServiceTests
{
[Fact]
public async Task Place_EnoughStock_ReducesStock()
{
var store = new FakeProductStore();
store.Products.Add(new Product
{
Id = 1, Name = "Bút bi", Stock = 120
});
var service = new OrderService(store);
bool ok = await service.PlaceAsync(1, 20);
Assert.True(ok);
Assert.Equal(100, store.Products[0].Stock);
}
}FindgiốngFindAllnhưng chỉ trả về phần tử đầu tiên thoả điều kiện, không có thì trả vềnull.- Test có
awaitthì khai báoasync Task, như bài async/await cơ bản. - Arrange tạo fake và nạp đúng dữ liệu test cần, không phụ thuộc database.
SetStockCallsđếm số lần service gọiSetStockAsync, để test kiểm tra được service có ghi tồn kho hay không.Task.FromResultbọc sẵn kết quả thànhTask, như ở bài Tách giao diện và dữ liệu.
Thử ngay
Thêm test cho trường hợp hết hàng vào OrderServiceTests:
using Xunit;
public class OrderServiceTests
{
[Fact]
public async Task Place_OutOfStock_DoesNotSave()
{
var store = new FakeProductStore();
store.Products.Add(new Product
{
Id = 2, Name = "Vở", Stock = 0
});
var service = new OrderService(store);
bool ok = await service.PlaceAsync(2, 1);
Assert.False(ok);
Assert.Equal(0, store.SetStockCalls);
}
}Đoán trước khi chạy: test xanh hay đỏ? SetStockCalls bằng mấy?
Xem kết quả
Passed! - Failed: 0, ...Xanh. Vở có tồn kho 0 nên InStockAsync không trả về nó, service trả false
ngay mà không gọi SetStockAsync. SetStockCalls vẫn là 0, tức không có
lần ghi nào xuống kho.
Lỗi hay gặp
Quên await khi gọi method async trong test. PlaceAsync trả về
Task<bool> chứ không phải bool.
// SAI — lỗi compile CS1503: Task<bool> không phải bool
using Xunit;
public class OrderServiceTests
{
[Fact]
public void Place_NoAwait()
{
var service = new OrderService(
new FakeProductStore());
Assert.False(service.PlaceAsync(1, 5));
}
}// ĐÚNG — test async Task, await kết quả
using Xunit;
public class OrderServiceTests
{
[Fact]
public async Task Place_Unknown_ReturnsFalse()
{
var service = new OrderService(
new FakeProductStore());
Assert.False(await service.PlaceAsync(1, 5));
}
}Cũng đừng viết test async void: xUnit báo cảnh báo xUnit1048, vì các bản
sau sẽ bỏ hỗ trợ kiểu này.
Tóm tắt
- Fake implement cùng interface, giữ dữ liệu trong bộ nhớ.
- Class nhận interface qua constructor thì test truyền fake vào được.
- Fake có thể đếm lời gọi để kiểm tra "có ghi hay không".
- Test có
awaitthì khai báoasync Task, không dùngasync void.
Bình luận
Đang tải bình luận…