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 ProductStore. 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 ProductStore 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.
FakeReportSource ở bài Viết test cho API là một fake nhỏ. Bài này viết
fake cho ProductStore, thêm bộ đếm số lần ghi.
Như bài DIP và dependency injection, OrderService nhận interface
ProductStore qua constructor, nên truyền fake hay bản thật đều được.
Ví dụ
Class cần test, nằm trong src/main/java/com/shop/api/. ProductStore giữ
đúng như bài Truy vấn với JPA, Product thêm setId để test tự đặt mã:
// OrderService.java
package com.shop.api;
import java.util.List;
class OrderService {
private final ProductStore store;
public OrderService(ProductStore store) {
this.store = store;
}
public boolean place(long productId, int quantity) {
List<Product> inStock = store.inStock();
Product product = inStock.stream()
.filter(p -> p.getId() == productId)
.findFirst()
.orElse(null);
if (product == null
|| product.getStock() < quantity) {
return false;
}
store.setStock(productId,
product.getStock() - quantity);
return true;
}
}
// ProductStore.java
package com.shop.api;
import java.util.List;
interface ProductStore {
List<Product> inStock();
boolean setStock(long id, int stock);
}
// Product.java
package com.shop.api;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name = "";
private long price;
private int stock;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public long getPrice() { return price; }
public void setPrice(long price) {
this.price = price;
}
public int getStock() { return stock; }
public void setStock(int stock) {
this.stock = stock;
}
}Fake và test, nằm trong src/test/java/com/shop/api/:
// FakeProductStore.java
package com.shop.api;
import java.util.*;
class FakeProductStore implements ProductStore {
private final List<Product> products =
new ArrayList<>();
private int setStockCalls = 0;
public List<Product> getProducts() {
return products;
}
public int getSetStockCalls() {
return setStockCalls;
}
@Override
public List<Product> inStock() {
return products.stream()
.filter(p -> p.getStock() > 0)
.toList();
}
@Override
public boolean setStock(long id, int stock) {
setStockCalls++;
for (Product p : products) {
if (p.getId() == id) {
p.setStock(stock);
return true;
}
}
return false;
}
}
// OrderServiceTests.java
package com.shop.api;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
class OrderServiceTests {
@Test
public void placeEnoughStockReducesStock() {
var store = new FakeProductStore();
var pen = new Product();
pen.setId(1L);
pen.setName("Bút bi");
pen.setStock(120);
store.getProducts().add(pen);
var service = new OrderService(store);
boolean ok = service.place(1, 20);
assertTrue(ok);
assertEquals(100, pen.getStock());
}
}findFirst()lấy phần tử đầu tiên thoả điều kiện, trả vềOptional.orElse(null)cho ranullkhi không có phần tử nào.- 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ọisetStock, để test kiểm tra được service có ghi tồn kho hay không.- Chỉ test gọi
setId. App thật vẫn để Oracle tự sinh mã khi lưu.
Thử ngay
Thêm test cho trường hợp hết hàng vào OrderServiceTests:
// OrderServiceTests.java
package com.shop.api;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
class OrderServiceTests {
@Test
public void placeOutOfStockDoesNotSave() {
var store = new FakeProductStore();
var book = new Product();
book.setId(2L);
book.setName("Vở");
book.setStock(0);
store.getProducts().add(book);
var service = new OrderService(store);
boolean ok = service.place(2, 1);
assertFalse(ok);
assertEquals(0, store.getSetStockCalls());
}
}Đoán trước khi chạy: test xanh hay đỏ? getSetStockCalls() bằng mấy?
Xem kết quả
[INFO] Running com.shop.api.OrderServiceTests
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.021 s -- in com.shop.api.OrderServiceTestsXanh. Vở có tồn kho 0 nên inStock() không trả về nó, service trả false
ngay mà không gọi setStock. getSetStockCalls() vẫn là 0, tức không có
lần ghi nào xuống kho.
Lỗi hay gặp
Để method test là private. JUnit không chạy method này. Test sai vẫn
không bao giờ đỏ, và dễ bỏ sót dòng cảnh báo lẫn trong log.
// SAI — method private, JUnit bỏ qua test này
// OrderServiceTests.java
package com.shop.api;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
class OrderServiceTests {
@Test
private void placeUnknownReturnsFalse() {
var service = new OrderService(
new FakeProductStore());
assertFalse(service.place(1, 5));
}
}// ĐÚNG — method test public void
// OrderServiceTests.java
package com.shop.api;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
class OrderServiceTests {
@Test
public void placeUnknownReturnsFalse() {
var service = new OrderService(
new FakeProductStore());
assertFalse(service.place(1, 5));
}
}Với bản SAI, ./mvnw test chỉ in cảnh báo @Test method ... must not be private. It will not be executed. rồi vẫn báo xanh.
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".
- Method test khai báo
public void, không đểprivate.
Tự kiểm tra
0/3 câuThêm method findById vào ProductStore. FakeProductStore trong src/test sẽ thế nào?
Mọi test OrderService dùng fake đều xanh, nhưng app thật vẫn trừ kho sai trên Oracle. Vì sao có thể như vậy?
Method test nên khai báo thế nào để JUnit chạy nó?