×
¥
查看详情
🔥 会员专享 文生代码 测试

全面单元测试生成

👁️ 521 次查看
📅 Nov 24, 2025
💡 核心价值: 根据提供的代码片段与测试场景生成高质量单元测试用例,涵盖正向、反向、边界条件及依赖项模拟,支持常见测试框架及语言,可直接集成CI/CD流水线,提升测试覆盖率与维护效率,降低手动编写与回归成本。

🎯 可自定义参数(6个)

测试框架
用于指定生成单元测试的框架
编程语言
用于匹配代码语法和测试生成逻辑
代码片段
用于提供需生成测试的函数或类代码
测试场景描述
用于说明需覆盖的业务场景、边界条件及异常情况
依赖与模拟需求
用于描述依赖项及模拟要求,支持复杂场景测试
覆盖深度
用于控制测试覆盖深度

🎨 效果示例

下面提供一组使用 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 == ""

说明:

  • 使用 pytest 和 pytest-mock 的 autospec 来严格模拟依赖,避免接口漂移。
  • 正向用例断言:保存对象内容、返回值、时间戳格式(包含 Z)、依赖调用次数与参数,并用 side_effect 断言调用顺序:exists_username -> save -> send_welcome。
  • 边界用例使用 parametrize 覆盖用户名长度最小/最大、推荐码最小/最大、密码恰好8位且包含字母数字。
  • 反向用例分别覆盖邮箱校验失败、密码过短、密码缺少字母或数字、非法推荐码、重复用户名,断言抛出特定错误消息且依赖不被调用,实现短路行为验证。
  • 使用 capsys 校验无额外输出,便于在 CI 并行执行和覆盖率统计。

下面是使用 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);
  });
});

说明:

  • 使用 jest.useFakeTimers 和 jest.setSystemTime 控制 Date.now,从而对 TTL 与回退定时进行精确推进。
  • 通过 jest.fn() 模拟 http.fetch 多种响应场景:200、4xx、5xx、reject(new Error('network')),以及支持 Abort 的慢请求。
  • 通过内存变量 store 作为简易缓存实现,配合 cache.get/set 的 mock 验证缓存命中、写入次数与 expiresAt=now+ttl 语义。
  • 深拷贝断言通过修改返回对象并比较缓存中存储的数据是否未被影响。
  • 在超时场景中对 AbortController.prototype.abort 进行 spy,验证 abort 调用次数与 fetch 的 signal 参数。
  • 包含正向与反向用例、边界条件(超时、最大重试、4xx不重试、forceRefresh)与依赖项模拟,适合并入 CI 并生成稳定覆盖率。

示例详情

该提示词已被收录:
“程序员必备:提升开发效率的专业AI提示词合集”
让 AI 成为你的第二双手,从代码生成到测试文档全部搞定,节省 80% 开发时间
√ 立即可用 · 零学习成本
√ 参数化批量生成
√ 专业提示词工程师打磨

📖 如何使用

30秒出活:复制 → 粘贴 → 搞定
与其花几十分钟和AI聊天、试错,不如直接复制这些经过千人验证的模板,修改几个 {{变量}} 就能立刻获得专业级输出。省下来的时间,足够你轻松享受两杯咖啡!
加载中...
💬 不会填参数?让 AI 反过来问你
不确定变量该填什么?一键转为对话模式,AI 会像资深顾问一样逐步引导你,问几个问题就能自动生成完美匹配你需求的定制结果。零门槛,开口就行。
转为对话模式
🚀 告别复制粘贴,Chat 里直接调用
无需切换,输入 / 唤醒 8000+ 专家级提示词。 插件将全站提示词库深度集成于 Chat 输入框。基于当前对话语境,系统智能推荐最契合的 Prompt 并自动完成参数化,让海量资源触手可及,从此彻底告别"手动搬运"。
即将推出
🔌 接口一调,提示词自己会进化
手动跑一次还行,跑一百次呢?通过 API 接口动态注入变量,接入批量评价引擎,让程序自动迭代出更高质量的提示词方案。Prompt 会自己进化,你只管收结果。
发布 API
🤖 一键变成你的专属 Agent 应用
不想每次都配参数?把这条提示词直接发布成独立 Agent,内嵌图片生成、参数优化等工具,分享链接就能用。给团队或客户一个"开箱即用"的完整方案。
创建 Agent

✅ 特性总结

快速生成高质量单元测试,针对不同编程语言和测试框架自由适配。
智能识别代码逻辑,提供完整正向与反向测试用例,确保测试覆盖面全面。
支持边界条件测试和依赖项模拟,轻松覆盖复杂场景中的潜在问题。
针对具体业务场景(如支付、用户登录等)灵活定制测试内容和用例。
精准匹配测试框架和语言特色,无需繁琐配置,一键生成直接使用。
提供可拓展模板,用户可以轻松自定义和复用测试生成规则。
优化生成的测试代码可读性,便于开发团队快速理解与修改。
减少手动编写测试代码的时间和精力,让团队聚焦核心开发任务。

🎯 解决的问题

帮助开发人员快速生成高质量的单元测试代码,大幅提升代码覆盖率,节约时间和人力成本。

🕒 版本历史

当前版本
v2.1 2024-01-15
优化输出结构,增强情节连贯性
  • ✨ 新增章节节奏控制参数
  • 🔧 优化人物关系描述逻辑
  • 📝 改进主题深化引导语
  • 🎯 增强情节转折点设计
v2.0 2023-12-20
重构提示词架构,提升生成质量
  • 🚀 全新的提示词结构设计
  • 📊 增加输出格式化选项
  • 💡 优化角色塑造引导
v1.5 2023-11-10
修复已知问题,提升稳定性
  • 🐛 修复长文本处理bug
  • ⚡ 提升响应速度
v1.0 2023-10-01
首次发布
  • 🎉 初始版本上线
COMING SOON
版本历史追踪,即将启航
记录每一次提示词的进化与升级,敬请期待。

💬 用户评价

4.8
⭐⭐⭐⭐⭐
基于 28 条评价
5星
85%
4星
12%
3星
3%
👤
电商运营 - 张先生
⭐⭐⭐⭐⭐ 2025-01-15
双十一用这个提示词生成了20多张海报,效果非常好!点击率提升了35%,节省了大量设计时间。参数调整很灵活,能快速适配不同节日。
效果好 节省时间
👤
品牌设计师 - 李女士
⭐⭐⭐⭐⭐ 2025-01-10
作为设计师,这个提示词帮我快速生成创意方向,大大提升了工作效率。生成的海报氛围感很强,稍作调整就能直接使用。
创意好 专业
COMING SOON
用户评价与反馈系统,即将上线
倾听真实反馈,在这里留下您的使用心得,敬请期待。
加载中...