VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Kiến trúc và chất lượng codeTest
Bài 6/17
6 phút

Fake thay phụ thuộc

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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.

app thật test OrderService ProductStore DbProductStore FakeProductStore
OrderService chỉ biết ProductStore, test truyền fake vào

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 ra null khi 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ọi setStock, để 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.OrderServiceTests

Xanh. 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âu
Câu 1

Thêm method findById vào ProductStore. FakeProductStore trong src/test sẽ thế nào?

Câu 2

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?

Câu 3

Method test nên khai báo thế nào để JUnit chạy nó?

Parameterized test và nhiều trường hợpTest đáng tin

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt