热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
根据提供的代码片段与测试场景生成高质量单元测试用例,涵盖正向、反向、边界条件及依赖项模拟,支持常见测试框架及语言,可直接集成CI/CD流水线,提升测试覆盖率与维护效率,降低手动编写与回归成本。
下面提供一组使用 pytest 与 pytest-mock 的单元测试示例,覆盖正向、边界和反向场景,并对依赖进行模拟与交互断言。请将 your_module 替换为实际模块名。
文件名示例:tests/test_user_service.py
import pytest from datetime import datetime from unittest.mock import call
from your_module import ( UserService, User, EmailValidator, UserRepository, Clock, Mailer, )
@pytest.fixture def fixed_now(): return datetime(2024, 1, 2, 3, 4, 5)
@pytest.fixture def make_service(mocker, fixed_now): def _make(email_valid=True, exists=False, save_id="u_123"): repo = mocker.create_autospec(UserRepository, instance=True) email_validator = mocker.create_autospec(EmailValidator, instance=True) clock = mocker.create_autospec(Clock, instance=True) mailer = mocker.create_autospec(Mailer, instance=True)
email_validator.is_valid.return_value = email_valid
repo.exists_username.return_value = exists
repo.save.return_value = save_id
clock.now.return_value = fixed_now
service = UserService(repo, email_validator, clock, mailer)
return service, repo, email_validator, clock, mailer
return _make
def test_register_user_success_order_and_fields(mocker, fixed_now, make_service, capsys): service, repo, email_validator, clock, mailer = make_service()
# 断言调用顺序:exists_username -> save -> send_welcome
def save_side_effect(user):
assert repo.exists_username.call_count == 1
return "u_123"
repo.save.side_effect = save_side_effect
def send_welcome_side_effect(email, username):
# send_welcome 调用前应已 save 一次
assert repo.save.call_count == 1
saved_user = repo.save.call_args[0][0]
assert saved_user.username == username
assert saved_user.email == email
mailer.send_welcome.side_effect = send_welcome_side_effect
result = service.register_user(
username="john_doe",
email="john@example.com",
password="Abc12345",
referral_code="ABC123"
)
assert result == {
"id": "u_123",
"username": "john_doe",
"created_at": fixed_now.isoformat() + "Z",
"referral_code": "ABC123",
}
email_validator.is_valid.assert_called_once_with("john@example.com")
repo.exists_username.assert_called_once_with("john_doe")
repo.save.assert_called_once()
# 校验保存的 User 对象
saved_user = repo.save.call_args[0][0]
assert isinstance(saved_user, User)
assert saved_user.id is None
assert saved_user.username == "john_doe"
assert saved_user.email == "john@example.com"
assert saved_user.created_at == fixed_now
assert saved_user.referral_code == "ABC123"
mailer.send_welcome.assert_called_once_with("john@example.com", "john_doe")
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
@pytest.mark.parametrize("username", ["abc", "a" * 20]) @pytest.mark.parametrize("referral", ["ABCDEF", "A" * 12]) def test_register_user_boundary_username_and_referral(mocker, fixed_now, make_service, username, referral, capsys): service, repo, email_validator, clock, mailer = make_service()
result = service.register_user(
username=username,
email="u@example.com",
password="Abc12345", # 正好8,包含字母和数字
referral_code=referral
)
assert result["id"] == "u_123"
assert result["username"] == username
assert result["created_at"] == fixed_now.isoformat() + "Z"
assert result["referral_code"] == referral
email_validator.is_valid.assert_called_once_with("u@example.com")
repo.exists_username.assert_called_once_with(username)
repo.save.assert_called_once()
mailer.send_welcome.assert_called_once_with("u@example.com", username)
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
def test_register_user_success_no_referral(mocker, fixed_now, make_service, capsys): service, repo, email_validator, clock, mailer = make_service()
result = service.register_user(
username="user_123",
email="ok@example.com",
password="Abc12345",
referral_code=None
)
assert result == {
"id": "u_123",
"username": "user_123",
"created_at": fixed_now.isoformat() + "Z",
"referral_code": None,
}
email_validator.is_valid.assert_called_once_with("ok@example.com")
repo.exists_username.assert_called_once_with("user_123")
repo.save.assert_called_once()
mailer.send_welcome.assert_called_once_with("ok@example.com", "user_123")
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
def test_register_user_invalid_email_short_circuit(mocker, make_service, capsys): service, repo, email_validator, clock, mailer = make_service(email_valid=False)
with pytest.raises(ValueError, match="invalid_email"):
service.register_user(username="john_doe", email="bad@", password="Abc12345", referral_code="ABC123")
email_validator.is_valid.assert_called_once_with("bad@")
repo.exists_username.assert_not_called()
repo.save.assert_not_called()
mailer.send_welcome.assert_not_called()
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
def test_register_user_weak_password_too_short_short_circuit(mocker, make_service, capsys): service, repo, email_validator, clock, mailer = make_service(email_valid=True)
with pytest.raises(ValueError, match="weak_password: length < 8"):
service.register_user(username="john_doe", email="john@example.com", password="Abc123", referral_code="ABC123")
email_validator.is_valid.assert_called_once_with("john@example.com")
repo.exists_username.assert_not_called()
repo.save.assert_not_called()
mailer.send_welcome.assert_not_called()
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
@pytest.mark.parametrize("pwd", ["abcdefgh", "12345678"]) def test_register_user_weak_password_missing_letters_or_digits_short_circuit(mocker, make_service, pwd, capsys): service, repo, email_validator, clock, mailer = make_service(email_valid=True)
with pytest.raises(ValueError, match="weak_password: must contain letters and digits"):
service.register_user(username="john_doe", email="john@example.com", password=pwd, referral_code="ABC123")
email_validator.is_valid.assert_called_once_with("john@example.com")
repo.exists_username.assert_not_called()
repo.save.assert_not_called()
mailer.send_welcome.assert_not_called()
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
@pytest.mark.parametrize("bad_referral", ["ABCDE", "abc!", "A" * 13]) def test_register_user_invalid_referral_short_circuit(mocker, make_service, bad_referral, capsys): service, repo, email_validator, clock, mailer = make_service(email_valid=True)
with pytest.raises(ValueError, match="invalid_referral_code"):
service.register_user(username="john_doe", email="john@example.com", password="Abc12345", referral_code=bad_referral)
email_validator.is_valid.assert_called_once_with("john@example.com")
repo.exists_username.assert_not_called()
repo.save.assert_not_called()
mailer.send_welcome.assert_not_called()
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
def test_register_user_username_taken(mocker, make_service, capsys): service, repo, email_validator, clock, mailer = make_service(email_valid=True, exists=True)
with pytest.raises(ValueError, match="username_taken"):
service.register_user(username="john_doe", email="john@example.com", password="Abc12345", referral_code="ABC123")
email_validator.is_valid.assert_called_once_with("john@example.com")
repo.exists_username.assert_called_once_with("john_doe")
repo.save.assert_not_called()
mailer.send_welcome.assert_not_called()
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == ""
说明:
下面是使用 JUnit 5 + Mockito 编写的全面单元测试示例,覆盖正向、边界与反向用例,并对依赖进行模拟与调用顺序验证。将该测试类放置在与被测类相同的包下或测试源路径中即可运行。
import static org.junit.jupiter.api.Assertions.; import static org.mockito.Mockito.;
import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(MockitoExtension.class) public class CheckoutServiceTest {
@Mock
InventoryGateway inventory;
@Mock
PaymentGateway payment;
@Mock
DiscountPolicy discountPolicy;
CheckoutService sut;
@BeforeEach
void setUp() {
sut = new CheckoutService(inventory, payment, discountPolicy);
}
private LineItem item(String sku, int qty, String unitPrice) {
return new LineItem(sku, qty, unitPrice == null ? null : new BigDecimal(unitPrice));
}
// 正向:STANDARD 无优惠券,含四舍五入校验,token校验
@Test
void checkout_standard_noCoupon_rounding_and_token() {
List<LineItem> items = Arrays.asList(
item("A", 2, "10.00"), // 20.00
item("B", 1, "5.125") // 5.125
);
Order order = new Order(items, null, Membership.STANDARD);
// subtotal raw = 25.125 -> round(2) HALF_UP -> 25.13
when(discountPolicy.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("25.13")), isNull()))
.thenReturn(BigDecimal.ZERO);
when(inventory.reserve("A", 2)).thenReturn(true);
when(inventory.reserve("B", 1)).thenReturn(true);
when(payment.preAuth(new BigDecimal("25.13"))).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(new BigDecimal("25.13"), r.getSubtotal(), "subtotal should be rounded to cents");
assertEquals(BigDecimal.ZERO, r.getDiscount(), "discount should be zero");
assertEquals(new BigDecimal("25.13"), r.getTotal(), "total should equal subtotal when discount is zero");
assertEquals("tok_abc", r.getPaymentToken());
// 验证调用顺序:先库存再支付
InOrder inorder = inOrder(inventory, payment);
inorder.verify(inventory).reserve("A", 2);
inorder.verify(inventory).reserve("B", 1);
inorder.verify(payment).preAuth(new BigDecimal("25.13"));
// 验证依赖调用次数与参数
verify(discountPolicy, times(1))
.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("25.13")), isNull());
verify(inventory, times(1)).reserve("A", 2);
verify(inventory, times(1)).reserve("B", 1);
verify(payment, times(1)).preAuth(new BigDecimal("25.13"));
}
// 正向:GOLD + coupon,折扣值带小数,校验 total 的四舍五入
@Test
void checkout_gold_with_coupon_discount_and_total_rounding() {
List<LineItem> items = Arrays.asList(
item("X", 3, "4.999"), // 14.997
item("Y", 2, "5.066") // 10.132 -> subtotal raw: 25.129 -> round -> 25.13
);
Order order = new Order(items, "SAVE5", Membership.GOLD);
BigDecimal expectedSubtotal = new BigDecimal("25.13");
BigDecimal discount = new BigDecimal("5.432"); // arbitrary discount with extra precision
when(discountPolicy.discountFor(eq(Membership.GOLD), eq(expectedSubtotal), eq("SAVE5")))
.thenReturn(discount);
when(inventory.reserve("X", 3)).thenReturn(true);
when(inventory.reserve("Y", 2)).thenReturn(true);
// total = 25.13 - 5.432 = 19.698 -> round(2) = 19.70
BigDecimal expectedTotal = new BigDecimal("19.70");
when(payment.preAuth(expectedTotal)).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(expectedSubtotal, r.getSubtotal());
assertEquals(discount, r.getDiscount());
assertEquals(expectedTotal, r.getTotal());
assertEquals("tok_abc", r.getPaymentToken());
InOrder inorder = inOrder(inventory, payment);
inorder.verify(inventory).reserve("X", 3);
inorder.verify(inventory).reserve("Y", 2);
inorder.verify(payment).preAuth(expectedTotal);
verify(discountPolicy).discountFor(eq(Membership.GOLD), eq(expectedSubtotal), eq("SAVE5"));
}
// 正向:PLATINUM + coupon,折扣等于subtotal -> total 为 0.00
@Test
void checkout_platinum_max_discount_total_zero() {
List<LineItem> items = Arrays.asList(
item("P", 1, "100.00")
);
Order order = new Order(items, "MAX", Membership.PLATINUM);
BigDecimal expectedSubtotal = new BigDecimal("100.00");
when(discountPolicy.discountFor(eq(Membership.PLATINUM), eq(expectedSubtotal), eq("MAX")))
.thenReturn(new BigDecimal("100.00"));
when(inventory.reserve("P", 1)).thenReturn(true);
when(payment.preAuth(new BigDecimal("0.00"))).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(expectedSubtotal, r.getSubtotal());
assertEquals(new BigDecimal("100.00"), r.getDiscount());
assertEquals(new BigDecimal("0.00"), r.getTotal());
assertEquals("tok_abc", r.getPaymentToken());
InOrder inorder = inOrder(inventory, payment);
inorder.verify(inventory).reserve("P", 1);
inorder.verify(payment).preAuth(new BigDecimal("0.00"));
}
// 边界:空订单
@Test
void empty_order_throws() {
Order order = new Order(Collections.emptyList(), null, Membership.STANDARD);
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> sut.checkout(order));
assertEquals("empty_order", ex.getMessage());
verifyNoInteractions(inventory, payment, discountPolicy);
}
// 边界:数量为 0
@Test
void invalid_qty_throws_and_message_contains_sku() {
Order order = new Order(Arrays.asList(item("BADQTY", 0, "1.00")), null, Membership.STANDARD);
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> sut.checkout(order));
assertEquals("invalid_qty:BADQTY", ex.getMessage());
verifyNoInteractions(inventory, payment, discountPolicy);
}
// 边界:价格为 null
@Test
void invalid_price_null_throws_and_message_contains_sku() {
Order order = new Order(Arrays.asList(item("NULLPRICE", 1, null)), null, Membership.STANDARD);
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> sut.checkout(order));
assertEquals("invalid_price:NULLPRICE", ex.getMessage());
verifyNoInteractions(inventory, payment, discountPolicy);
}
// 边界:价格为负
@Test
void invalid_price_negative_throws_and_message_contains_sku() {
Order order = new Order(Arrays.asList(item("NEGPRICE", 1, "-0.01")), null, Membership.STANDARD);
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> sut.checkout(order));
assertEquals("invalid_price:NEGPRICE", ex.getMessage());
verifyNoInteractions(inventory, payment, discountPolicy);
}
// 边界:单件商品金额精度 0.005 -> subtotal 四舍五入到 0.01
@Test
void precision_boundary_0_005_rounds_up_to_cent() {
Order order = new Order(Arrays.asList(item("CENTS", 1, "0.005")), null, Membership.STANDARD);
when(discountPolicy.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("0.01")), isNull()))
.thenReturn(BigDecimal.ZERO);
when(inventory.reserve("CENTS", 1)).thenReturn(true);
when(payment.preAuth(new BigDecimal("0.01"))).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(new BigDecimal("0.01"), r.getSubtotal());
assertEquals(BigDecimal.ZERO, r.getDiscount());
assertEquals(new BigDecimal("0.01"), r.getTotal());
assertEquals("tok_abc", r.getPaymentToken());
}
// 边界:折扣为负 -> 截断为 0
@Test
void negative_discount_truncated_to_zero() {
Order order = new Order(Arrays.asList(item("D1", 2, "5.00")), null, Membership.STANDARD); // subtotal 10.00
when(discountPolicy.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("10.00")), isNull()))
.thenReturn(new BigDecimal("-3.00"));
when(inventory.reserve("D1", 2)).thenReturn(true);
when(payment.preAuth(new BigDecimal("10.00"))).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(new BigDecimal("10.00"), r.getSubtotal());
assertEquals(BigDecimal.ZERO, r.getDiscount(), "negative discount should be coerced to zero");
assertEquals(new BigDecimal("10.00"), r.getTotal());
}
// 边界:折扣超额 -> 截断为 subtotal
@Test
void discount_exceeds_subtotal_clipped() {
Order order = new Order(Arrays.asList(item("D2", 1, "10.00")), null, Membership.GOLD);
when(discountPolicy.discountFor(eq(Membership.GOLD), eq(new BigDecimal("10.00")), isNull()))
.thenReturn(new BigDecimal("15.00")); // exceeds subtotal
when(inventory.reserve("D2", 1)).thenReturn(true);
when(payment.preAuth(new BigDecimal("0.00"))).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(new BigDecimal("10.00"), r.getSubtotal());
assertEquals(new BigDecimal("10.00"), r.getDiscount(), "discount should be clipped to subtotal");
assertEquals(new BigDecimal("0.00"), r.getTotal());
}
// 边界:大数量聚合
@Test
void large_quantity_aggregation() {
Order order = new Order(Arrays.asList(item("BULK", 1000, "0.01")), "BULK10", Membership.STANDARD); // subtotal 10.00
when(discountPolicy.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("10.00")), eq("BULK10")))
.thenReturn(BigDecimal.ZERO);
when(inventory.reserve("BULK", 1000)).thenReturn(true);
when(payment.preAuth(new BigDecimal("10.00"))).thenReturn("tok_abc");
Receipt r = sut.checkout(order);
assertEquals(new BigDecimal("10.00"), r.getSubtotal());
assertEquals(BigDecimal.ZERO, r.getDiscount());
assertEquals(new BigDecimal("10.00"), r.getTotal());
assertEquals("tok_abc", r.getPaymentToken());
verify(inventory).reserve("BULK", 1000);
verify(payment).preAuth(new BigDecimal("10.00"));
}
// 反向:库存保留返回 false -> OutOfStockException,且不调用支付
@Test
void inventory_reserve_false_should_throw_and_not_call_payment() {
List<LineItem> items = Arrays.asList(item("I1", 1, "3.00"), item("I2", 2, "4.00")); // subtotal 11.00
Order order = new Order(items, null, Membership.STANDARD);
when(discountPolicy.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("11.00")), isNull()))
.thenReturn(BigDecimal.ZERO);
when(inventory.reserve("I1", 1)).thenReturn(true);
when(inventory.reserve("I2", 2)).thenReturn(false); // triggers reserve_failed
OutOfStockException ex = assertThrows(OutOfStockException.class, () -> sut.checkout(order));
assertEquals("reserve_failed:I2", ex.getMessage());
verify(payment, times(0)).preAuth(any());
// 验证库存调用发生
verify(inventory).reserve("I1", 1);
verify(inventory).reserve("I2", 2);
}
// 反向:库存抛出异常 -> 冒泡,且不调用支付
@Test
void inventory_reserve_throws_should_bubble_and_not_call_payment() {
List<LineItem> items = Arrays.asList(item("J1", 1, "1.00"), item("J2", 1, "2.00")); // subtotal 3.00
Order order = new Order(items, null, Membership.STANDARD);
when(discountPolicy.discountFor(eq(Membership.STANDARD), eq(new BigDecimal("3.00")), isNull()))
.thenReturn(BigDecimal.ZERO);
when(inventory.reserve("J1", 1)).thenReturn(true);
when(inventory.reserve("J2", 1)).thenThrow(new OutOfStockException("no_stock"));
OutOfStockException ex = assertThrows(OutOfStockException.class, () -> sut.checkout(order));
assertEquals("no_stock", ex.getMessage());
verify(payment, never()).preAuth(any());
verify(inventory).reserve("J1", 1);
verify(inventory).reserve("J2", 1);
}
// 反向:支付异常 -> 库存调用不受影响,异常冒泡
@Test
void payment_exception_propagates_and_inventory_calls_remain() {
List<LineItem> items = Arrays.asList(item("K1", 2, "2.50")); // subtotal 5.00
Order order = new Order(items, "C", Membership.GOLD);
when(discountPolicy.discountFor(eq(Membership.GOLD), eq(new BigDecimal("5.00")), eq("C")))
.thenReturn(new BigDecimal("1.25"));
when(inventory.reserve("K1", 2)).thenReturn(true);
BigDecimal expectedTotal = new BigDecimal("3.75"); // 5.00 - 1.25
when(payment.preAuth(expectedTotal)).thenThrow(new PaymentException("fail"));
PaymentException ex = assertThrows(PaymentException.class, () -> sut.checkout(order));
assertEquals("fail", ex.getMessage());
// 库存已调用,支付被调用一次但抛异常
verify(inventory).reserve("K1", 2);
verify(payment).preAuth(expectedTotal);
// 调用顺序验证
InOrder inorder = inOrder(inventory, payment);
inorder.verify(inventory).reserve("K1", 2);
inorder.verify(payment).preAuth(expectedTotal);
}
}
下面给出可直接用于CI的 Jest 单元测试示例,覆盖所要求的正向/反向/重试/超时/缓存/forceRefresh 等场景。请将 require 路径替换为你项目中 DataFetcher 的实际路径。
文件名示例:data-fetcher.test.js
/* eslint-env jest */
const { DataFetcher } = require('./DataFetcher'); // 替换为实际路径
// 简易 AbortController 兜底(保证在老 Node 或 jsdom 环境也能跑)
if (typeof global.AbortController === 'undefined') {
class FakeSignal {
constructor() { this.aborted = false; this._listeners = []; }
addEventListener(evt, fn) {
if (evt === 'abort') this._listeners.push(fn);
}
}
class FakeAbortController {
constructor() { this.signal = new FakeSignal(); }
abort() {
this.signal.aborted = true;
this.signal._listeners.forEach(fn => {
try { fn(); } catch (_) {}
});
}
}
global.AbortController = FakeAbortController;
}
describe('DataFetcher getJson', () => {
let http;
let cache;
let df;
let store; // 模拟内存缓存
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-01T00:00:00.000Z'));
store = null;
cache = {
get: jest.fn((key) => store),
set: jest.fn((key, value) => { store = value; }),
};
http = {
fetch: jest.fn(),
};
df = new DataFetcher(http, cache); // 使用默认 now=Date.now(受 fake timers 控制)
});
afterEach(() => {
jest.useRealTimers();
jest.resetAllMocks();
});
test('正向:首次请求落库缓存且返回深拷贝;随后命中缓存不触发http.fetch,TTL到期后自动刷新', async () => {
const url = 'http://api/test';
const ttlMs = 10_000; // 短 TTL 便于测试
const data1 = { foo: 'bar', nested: { a: 1 } };
http.fetch.mockResolvedValueOnce({
status: 200,
json: async () => data1,
});
// 首次请求:写缓存并返回深拷贝
const res1 = await df.getJson(url, { ttlMs });
expect(http.fetch).toHaveBeenCalledTimes(1);
expect(http.fetch).toHaveBeenCalledWith(url, expect.objectContaining({ signal: expect.any(Object) }));
expect(cache.set).toHaveBeenCalledTimes(1);
const written1 = cache.set.mock.calls[0][1];
expect(written1.data).toEqual(data1);
expect(written1.expiresAt).toBe(Date.now() + ttlMs);
// 返回深拷贝验证:修改返回值不影响缓存
res1.nested.a = 999;
expect(store.data.nested.a).toBe(1);
// 随后命中缓存:不触发 http.fetch
const res2 = await df.getJson(url);
expect(http.fetch).toHaveBeenCalledTimes(1);
// 再次深拷贝验证
expect(res2).not.toBe(store.data);
res2.foo = 'baz';
expect(store.data.foo).toBe('bar');
// TTL 到期后自动刷新:推进时间,触发新请求
jest.advanceTimersByTime(ttlMs + 1);
http.fetch.mockResolvedValueOnce({
status: 200,
json: async () => ({ foo: 'new' }),
});
const res3 = await df.getJson(url);
expect(http.fetch).toHaveBeenCalledTimes(2);
expect(cache.set).toHaveBeenCalledTimes(2);
const written2 = cache.set.mock.calls[1][1];
expect(store.data.foo).toBe('new');
expect(written2.expiresAt).toBe(Date.now() + df.defaultTTL); // 本次未传 ttlMs,使用默认 TTL
});
test('重试与回退:5xx触发指数回退重试并在回退区间推进后成功', async () => {
const url = 'http://api/retry';
// 第一次 500,第二次 502,第三次成功
http.fetch
.mockResolvedValueOnce({ status: 500, json: async () => ({}) })
.mockResolvedValueOnce({ status: 502, json: async () => ({}) })
.mockResolvedValueOnce({ status: 200, json: async () => ({ ok: true }) });
const promise = df.getJson(url, { retry: 2 });
// 初始执行,立即调用一次
expect(http.fetch).toHaveBeenCalledTimes(1);
// 等待第一段指数回退:1000ms
jest.advanceTimersByTime(999);
expect(http.fetch).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(1);
await Promise.resolve(); // 让微任务队列推进
expect(http.fetch).toHaveBeenCalledTimes(2);
// 第二段指数回退:2000ms
jest.advanceTimersByTime(1999);
expect(http.fetch).toHaveBeenCalledTimes(2);
jest.advanceTimersByTime(1);
await Promise.resolve();
expect(http.fetch).toHaveBeenCalledTimes(3);
const res = await promise;
expect(res).toEqual({ ok: true });
expect(cache.set).toHaveBeenCalledTimes(1);
});
test('重试与回退:网络错误达到最大重试后抛出最后错误', async () => {
const url = 'http://api/retryfail';
http.fetch
.mockRejectedValueOnce(new Error('network'))
.mockRejectedValueOnce(new Error('network'))
.mockRejectedValueOnce(new Error('network'));
const promise = df.getJson(url, { retry: 2 });
// 第一段指数回退:1000ms
expect(http.fetch).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(1000);
await Promise.resolve();
expect(http.fetch).toHaveBeenCalledTimes(2);
// 第二段指数回退:2000ms
jest.advanceTimersByTime(2000);
await Promise.resolve();
expect(http.fetch).toHaveBeenCalledTimes(3);
await expect(promise).rejects.toThrow('network');
expect(cache.set).not.toHaveBeenCalled();
});
test('超时:较短 timeoutMs 触发 AbortController.abort 并重试后成功', async () => {
const url = 'http://api/timeout-success';
const abortSpy = jest.spyOn(global.AbortController.prototype, 'abort');
// 第一次:模拟慢请求,等待 10s 才会返回,但 timeoutMs=50ms 必触发 abort
const slowAbortFetch = jest.fn((u, opts) => {
const { signal } = opts || {};
return new Promise((resolve, reject) => {
if (signal) {
if (signal.aborted) return reject(new Error('abort'));
signal.addEventListener('abort', () => reject(new Error('abort')), { once: true });
}
setTimeout(() => resolve({ status: 200, json: async () => ({ shouldNotReach: true }) }), 10_000);
});
});
// 第二次:立即成功
const fastFetch = jest.fn(() =>
Promise.resolve({ status: 200, json: async () => ({ ok: true }) })
);
http.fetch
.mockImplementationOnce(slowAbortFetch)
.mockImplementationOnce(fastFetch);
const promise = df.getJson(url, { timeoutMs: 50, retry: 1 });
// 推进到超时,触发 abort
jest.advanceTimersByTime(50);
await Promise.resolve();
expect(abortSpy).toHaveBeenCalledTimes(1);
// 进入指数回退阶段:1000ms
jest.advanceTimersByTime(1000);
await Promise.resolve();
// 第二次快速成功
const res = await promise;
expect(res).toEqual({ ok: true });
expect(http.fetch).toHaveBeenCalledTimes(2);
abortSpy.mockRestore();
});
test('超时:所有尝试均超时最终失败并抛出 abort 错误', async () => {
const url = 'http://api/timeout-fail';
const abortSpy = jest.spyOn(global.AbortController.prototype, 'abort');
const slowAbortFetch = jest.fn((u, opts) => {
const { signal } = opts || {};
return new Promise((resolve, reject) => {
if (signal) {
if (signal.aborted) return reject(new Error('abort'));
signal.addEventListener('abort', () => reject(new Error('abort')), { once: true });
}
setTimeout(() => resolve({ status: 200, json: async () => ({ shouldNotReach: true }) }), 10_000);
});
});
http.fetch
.mockImplementationOnce(slowAbortFetch)
.mockImplementationOnce(slowAbortFetch); // 重试一次也超时
const promise = df.getJson(url, { timeoutMs: 50, retry: 1 });
// 第一次超时
jest.advanceTimersByTime(50);
await Promise.resolve();
expect(abortSpy).toHaveBeenCalledTimes(1);
// 回退 1000ms 后第二次启动并超时
jest.advanceTimersByTime(1000);
await Promise.resolve();
jest.advanceTimersByTime(50);
await Promise.resolve();
expect(abortSpy).toHaveBeenCalledTimes(2);
await expect(promise).rejects.toThrow('abort');
expect(http.fetch).toHaveBeenCalledTimes(2);
abortSpy.mockRestore();
});
test('非重试:4xx错误直接抛出且不再重试', async () => {
const url = 'http://api/4xx';
http.fetch.mockResolvedValueOnce({
status: 404,
json: async () => ({ msg: 'not found' }),
});
const promise = df.getJson(url, { retry: 3 });
// 不应进入回退计时(虽然我们推进也不应触发再次 fetch)
await expect(promise).rejects.toThrow('client_error:404');
expect(http.fetch).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(5000); // 推进计时也不会有更多调用
expect(http.fetch).toHaveBeenCalledTimes(1);
expect(cache.set).not.toHaveBeenCalled();
});
test('依赖模拟与语义校验:命中缓存不触发 http;forceRefresh=true 绕过缓存;expiresAt=now+ttl', async () => {
const url = 'http://api/force';
// 预写入缓存(未过期)
store = {
data: { v: 1 },
expiresAt: Date.now() + 60_000,
};
// 命中缓存:不触发 http
const resCached = await df.getJson(url);
expect(resCached).toEqual({ v: 1 });
expect(http.fetch).toHaveBeenCalledTimes(0);
// forceRefresh=true:即便缓存有效也应触发请求并重写缓存
const data2 = { v: 2 };
http.fetch.mockResolvedValueOnce({
status: 200,
json: async () => data2,
});
const resRefresh = await df.getJson(url, { forceRefresh: true, ttlMs: 5000 });
expect(http.fetch).toHaveBeenCalledTimes(1);
expect(resRefresh).toEqual(data2);
expect(cache.set).toHaveBeenCalledTimes(1);
const written = cache.set.mock.calls[0][1];
expect(written.data).toEqual(data2);
expect(written.expiresAt).toBe(Date.now() + 5000);
// 再次 forceRefresh:再次触发 http
http.fetch.mockResolvedValueOnce({
status: 200,
json: async () => ({ v: 3 }),
});
const resRefresh2 = await df.getJson(url, { forceRefresh: true });
expect(http.fetch).toHaveBeenCalledTimes(2);
expect(resRefresh2).toEqual({ v: 3 });
expect(cache.set).toHaveBeenCalledTimes(2);
});
});
说明:
帮助开发人员快速生成高质量的单元测试代码,大幅提升代码覆盖率,节约时间和人力成本。
帮助测试开发人员快速生成复杂系统的单元测试,覆盖更多场景并确保代码的高质量交付。
为全栈工程师在忙碌的开发任务中节省单元测试编写时间,为代码快速上线保驾护航。
为团队节省研发成本,同时提高测试覆盖率,保障产品质量,助力项目按时交付。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
免费获取高级提示词-优惠即将到期