全面单元测试生成

283 浏览
23 试用
5 购买
Nov 14, 2025更新

根据提供的代码片段与测试场景自动生成高质量、可运行的单元测试用例,涵盖正向、反向、边界条件及依赖项模拟。适配常见测试框架/语言,可输出完整测试文件与运行指引,便于直接集成到 CI 流水线,提升测试覆盖与维护效率,减少手动编写与回归成本。

以下是完整的 pytest 单元测试文件内容,以及运行指引。测试使用 pytest + pytest-mock + monkeypatch,对 requests.Session.get 进行桩件,覆盖正向、反向、边界、TTL 和环境变量相关场景,并严格校验调用次数、参数与异常路径。

文件:test_price_calculator.py

import pytest import requests import price_calculator as pc from price_calculator import PriceCalculator

class StubResponse: """自定义响应对象,支持 status_code、json()、raise_for_status() 和故障注入。""" def init(self, status_code=200, json_data=None, raise_http_error=False, json_exc=None): self.status_code = status_code self._json_data = json_data or {} self._raise_http_error = raise_http_error self._json_exc = json_exc

def raise_for_status(self):
    if self._raise_http_error or self.status_code >= 400:
        raise requests.HTTPError(f"HTTP {self.status_code}")

def json(self):
    if self._json_exc:
        raise self._json_exc
    return self._json_data

def test_percent_discount_final_and_cache_hit(mocker, monkeypatch): # 环境:覆盖 TAX_API_URL monkeypatch.setenv("TAX_API_URL", "https://tax.test.local") # 桩件 session.get 返回固定税率 session = mocker.Mock(spec=requests.Session) session.get.return_value = StubResponse(status_code=200, json_data={"rate": 0.2}) calc = PriceCalculator(session=session)

items = [
    {"price": 19.99, "qty": 2},  # 39.98
    {"price": 5.50, "qty": 1},   # 45.48
    {"price": 1.25, "qty": 3},   # 49.23
]
discount = {"type": "percent", "value": 10}  # 10% 折扣

total1 = calc.calculate_final(items, "US", discount)
total2 = calc.calculate_final(items, "US", discount)

# 校验两位小数和总价
assert total1 == 53.17
assert total2 == 53.17
assert f"{total1:.2f}" == "53.17"

# 缓存命中:仅一次 HTTP 请求
assert session.get.call_count == 1
session.get.assert_called_once_with("https://tax.test.local/v1/tax/US", timeout=2)

def test_unknown_discount_type_raises_valueerror(mocker): session = mocker.Mock(spec=requests.Session) calc = PriceCalculator(session=session) items = [{"price": 10.0, "qty": 1}] discount = {"type": "oops", "value": 5} with pytest.raises(ValueError) as exc: calc.calculate_final(items, "US", discount) assert "unknown discount type" in str(exc.value) # 在折扣类型校验前抛出,不应触发税率查询 session.get.assert_not_called()

def test_negative_discount_value_raises_valueerror(mocker): session = mocker.Mock(spec=requests.Session) calc = PriceCalculator(session=session) items = [{"price": 10.0, "qty": 1}] discount = {"type": "percent", "value": -5} with pytest.raises(ValueError) as exc: calc.calculate_final(items, "US", discount) assert "discount value cannot be negative" in str(exc.value) session.get.assert_not_called()

def test_negative_qty_raises_valueerror(mocker): session = mocker.Mock(spec=requests.Session) calc = PriceCalculator(session=session) items = [{"price": 10.0, "qty": -1}] discount = {"type": "absolute", "value": 1} with pytest.raises(ValueError) as exc: calc.calculate_final(items, "US", discount) assert "invalid qty at index 0" in str(exc.value) session.get.assert_not_called()

def test_negative_price_raises_valueerror(mocker): session = mocker.Mock(spec=requests.Session) calc = PriceCalculator(session=session) items = [{"price": -10.0, "qty": 1}] discount = {"type": "absolute", "value": 1} with pytest.raises(ValueError) as exc: calc.calculate_final(items, "US", discount) assert "invalid price at index 0" in str(exc.value) session.get.assert_not_called()

def test_empty_items_total_zero(mocker, monkeypatch): # 环境可设置但结果应为 0.00 monkeypatch.setenv("TAX_API_URL", "https://tax.empty.local") session = mocker.Mock(spec=requests.Session) session.get.return_value = StubResponse(json_data={"rate": 0.25}) calc = PriceCalculator(session=session)

total = calc.calculate_final([], "GB", {"type": "percent", "value": 10})
assert total == 0.0
# 仍会进行税率查询,但总价为 0
session.get.assert_called_once_with("https://tax.empty.local/v1/tax/GB", timeout=2)

def test_absolute_discount_clamps_to_zero_when_negative(mocker): session = mocker.Mock(spec=requests.Session) session.get.return_value = StubResponse(json_data={"rate": 0.2}) calc = PriceCalculator(session=session)

items = [{"price": 10.0, "qty": 1}]  # subtotal = 10
discount = {"type": "absolute", "value": 50}  # 使小计变负
total = calc.calculate_final(items, "US", discount)
assert total == 0.0  # 归 0 后税也为 0

def test_tax_api_http_error_raises_runtimeerror(mocker): session = mocker.Mock(spec=requests.Session) session.get.return_value = StubResponse(status_code=500, raise_http_error=True) calc = PriceCalculator(session=session)

with pytest.raises(RuntimeError) as exc:
    calc.calculate_final([{"price": 10.0, "qty": 1}], "US", {"type": "absolute", "value": 1})
msg = str(exc.value)
assert "tax lookup failed" in msg
assert "HTTP 500" in msg
assert session.get.call_count == 1

def test_tax_api_timeout_raises_runtimeerror(mocker): session = mocker.Mock(spec=requests.Session) session.get.side_effect = requests.Timeout("connection timed out") calc = PriceCalculator(session=session)

with pytest.raises(RuntimeError) as exc:
    calc.calculate_final([{"price": 10.0, "qty": 1}], "US", {"type": "absolute", "value": 1})
assert "tax lookup failed" in str(exc.value)
assert session.get.call_count == 1

def test_tax_api_bad_json_raises_runtimeerror(mocker): session = mocker.Mock(spec=requests.Session) session.get.return_value = StubResponse(json_exc=ValueError("bad json")) calc = PriceCalculator(session=session)

with pytest.raises(RuntimeError) as exc:
    calc.calculate_final([{"price": 10.0, "qty": 1}], "US", {"type": "percent", "value": 10})
assert "tax lookup failed" in str(exc.value)
assert session.get.call_count == 1

def test_cache_ttl_expiry_triggers_second_http_request(mocker, monkeypatch): session = mocker.Mock(spec=requests.Session) # 两次调用返回相同税率,便于验证二次请求 session.get.side_effect = [ StubResponse(json_data={"rate": 0.1}), StubResponse(json_data={"rate": 0.1}), ] calc = PriceCalculator(session=session, cache_ttl_seconds=300)

# 控制 time.time,先在缓存有效期内,再推进到过期之后
now = {"t": 1000.0}
monkeypatch.setattr(pc.time, "time", lambda: now["t"])

# 第一次取税率 -> 触发 HTTP
rate1 = calc.get_tax_rate("FR")
assert rate1 == 0.1
assert session.get.call_count == 1

# 缓存未过期 -> 不触发 HTTP
now["t"] = 1000.0 + calc.cache_ttl_seconds - 1
rate2 = calc.get_tax_rate("FR")
assert rate2 == 0.1
assert session.get.call_count == 1

# 缓存过期 -> 再次触发 HTTP
now["t"] = 1000.0 + calc.cache_ttl_seconds + 1
rate3 = calc.get_tax_rate("FR")
assert rate3 == 0.1
assert session.get.call_count == 2

# 校验请求参数
expected_url = f"{calc.base_url}/v1/tax/FR"
assert session.get.call_args_list[0] == mocker.call(expected_url, timeout=2)
assert session.get.call_args_list[1] == mocker.call(expected_url, timeout=2)

def test_env_tax_api_url_used_in_request(mocker, monkeypatch): monkeypatch.setenv("TAX_API_URL", "https://api.override.local") session = mocker.Mock(spec=requests.Session) session.get.return_value = StubResponse(json_data={"rate": 0.15}) calc = PriceCalculator(session=session)

rate = calc.get_tax_rate("DE")
assert rate == 0.15
session.get.assert_called_once_with("https://api.override.local/v1/tax/DE", timeout=2)

pytest 运行指引:

  • 确保项目结构中包含 price_calculator.py 和上述测试文件 test_price_calculator.py。
  • 安装依赖:
    • pip install pytest pytest-mock requests
  • 在项目根目录执行:
    • pytest -q
  • 说明:
    • 测试使用 pytest-mock 的 mocker fixture 进行桩件及调用断言;
    • 使用 monkeypatch 控制环境变量和 time.time;
    • 所有 HTTP 交互均通过桩件完成,不会真实访问网络。

下面给出使用 JUnit 5 + Mockito 的完整、严格覆盖的单元测试类,放置路径建议为:src/test/java/app/order/OrderProcessorTest.java(与被测类在同一包 app.order 下,以便访问包级可见的类型)。

代码: // File: src/test/java/app/order/OrderProcessorTest.java package app.order;

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays; import java.util.List;

import static org.junit.jupiter.api.Assertions.; import static org.mockito.ArgumentMatchers.; import static org.mockito.Mockito.*; import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class) class OrderProcessorTest {

@Mock PaymentGateway paymentGateway;
@Mock InventoryService inventoryService;
@Mock OrderRepository orderRepository;
@Mock AuditLogger auditLogger;

OrderProcessor processor;

@BeforeEach
void setUp() {
    processor = new OrderProcessor(paymentGateway, inventoryService, orderRepository, auditLogger);
}

// 1) 正向:三明细成功,校验 reserve 调用、金额两位小数(四舍五入),markProcessed 与日志,返回 SUCCESS
@Test
void process_success_threeItems_reserveCalled_amountRounded_markProcessed_andLogged() {
    String orderId = "o-success";
    // 0.335 * 3 = 1.005,四舍五入到两位应为 1.01
    List<LineItem> items = Arrays.asList(
            new LineItem("SKU-A", 1, 0.335),
            new LineItem("SKU-B", 1, 0.335),
            new LineItem("SKU-C", 1, 0.335)
    );
    Order order = new Order(orderId, items);

    when(orderRepository.isProcessed(orderId)).thenReturn(false);
    when(paymentGateway.charge(eq(orderId), anyDouble()))
            .thenReturn(new ChargeResult(true, false, "TX123"));

    String result = processor.process(order);
    assertEquals("SUCCESS", result);

    // 交互顺序:reserve -> charge -> markProcessed -> log processed
    InOrder inOrder = inOrder(inventoryService, paymentGateway, orderRepository, auditLogger);
    inOrder.verify(inventoryService).reserve("SKU-A", 1);
    inOrder.verify(inventoryService).reserve("SKU-B", 1);
    inOrder.verify(inventoryService).reserve("SKU-C", 1);
    inOrder.verify(paymentGateway).charge(eq(orderId), argThat(a -> Math.abs(a - 1.01) < 1e-9));
    inOrder.verify(orderRepository).markProcessed(orderId);

    ArgumentCaptor<String> logCaptor = ArgumentCaptor.forClass(String.class);
    inOrder.verify(auditLogger).log(logCaptor.capture());
    String processedLog = logCaptor.getValue();
    assertTrue(processedLog.contains("processed:" + orderId + ":txn=TX123"));

    // 不应释放库存
    verify(inventoryService, never()).release(anyString(), anyInt());
    // 金额精度再断言(两位小数)
    verify(paymentGateway).charge(eq(orderId), argThat(a -> Math.abs(a - 1.01) < 1e-9));
}

// 2) 反向:支付失败时释放所有库存并抛 PaymentException
@Test
void process_paymentFails_releasesAllInventory_andThrowsPaymentException() {
    String orderId = "o-fail";
    List<LineItem> items = Arrays.asList(
            new LineItem("SKU-1", 2, 10.0),
            new LineItem("SKU-2", 1, 5.5),
            new LineItem("SKU-3", 3, 2.0)
    );
    Order order = new Order(orderId, items);

    when(orderRepository.isProcessed(orderId)).thenReturn(false);
    when(paymentGateway.charge(eq(orderId), anyDouble()))
            .thenReturn(new ChargeResult(false, false, null)); // 非 transient,直接失败

    Executable exec = () -> processor.process(order);
    PaymentException ex = assertThrows(PaymentException.class, exec);
    assertEquals("payment failed", ex.getMessage());

    // reserve 应被按明细调用
    verify(inventoryService).reserve("SKU-1", 2);
    verify(inventoryService).reserve("SKU-2", 1);
    verify(inventoryService).reserve("SKU-3", 3);

    // 交互顺序:reserve 全部 -> charge -> release 全部
    InOrder inOrder = inOrder(inventoryService, paymentGateway);
    inOrder.verify(inventoryService).reserve("SKU-1", 2);
    inOrder.verify(inventoryService).reserve("SKU-2", 1);
    inOrder.verify(inventoryService).reserve("SKU-3", 3);
    inOrder.verify(paymentGateway).charge(eq(orderId), anyDouble());
    inOrder.verify(inventoryService).release("SKU-1", 2);
    inOrder.verify(inventoryService).release("SKU-2", 1);
    inOrder.verify(inventoryService).release("SKU-3", 3);

    // 不应标记已处理
    verify(orderRepository, never()).markProcessed(anyString());
    // 支付仅调用一次
    verify(paymentGateway, times(1)).charge(eq(orderId), anyDouble());
}

// 2) 反向:非法明细(零数量)抛 IllegalArgumentException,且不调用库存与支付
@Test
void process_invalidItem_zeroQty_throwsIllegalArgumentException_andNoSideEffects() {
    String orderId = "o-bad-qty";
    List<LineItem> items = Arrays.asList(
            new LineItem("BAD-ZERO", 0, 1.0)
    );
    Order order = new Order(orderId, items);

    when(orderRepository.isProcessed(orderId)).thenReturn(false);

    assertThrows(IllegalArgumentException.class, () -> processor.process(order));

    verifyNoInteractions(paymentGateway);
    verifyNoInteractions(inventoryService);
    verify(orderRepository, never()).markProcessed(anyString());
}

// 2) 反向:非法明细(负价)抛 IllegalArgumentException,且不调用库存与支付
@Test
void process_invalidItem_negativePrice_throwsIllegalArgumentException_andNoSideEffects() {
    String orderId = "o-bad-price";
    List<LineItem> items = Arrays.asList(
            new LineItem("BAD-PRICE", 1, -0.01)
    );
    Order order = new Order(orderId, items);

    when(orderRepository.isProcessed(orderId)).thenReturn(false);

    assertThrows(IllegalArgumentException.class, () -> processor.process(order));

    verifyNoInteractions(paymentGateway);
    verifyNoInteractions(inventoryService);
    verify(orderRepository, never()).markProcessed(anyString());
}

// 3) 边界:幂等路径返回 ALREADY,且不触发支付与库存
@Test
void process_idempotentAlreadyProcessed_returnsAlready_noInventoryNoPayment() {
    String orderId = "o-idem";
    List<LineItem> items = Arrays.asList(
            new LineItem("SKU-X", 1, 2.0)
    );
    Order order = new Order(orderId, items);

    when(orderRepository.isProcessed(orderId)).thenReturn(true);

    String result = processor.process(order);
    assertEquals("ALREADY", result);

    verify(paymentGateway, never()).charge(anyString(), anyDouble());
    verify(inventoryService, never()).reserve(anyString(), anyInt());
    verify(inventoryService, never()).release(anyString(), anyInt());
    verify(orderRepository, never()).markProcessed(anyString());

    ArgumentCaptor<String> logCaptor = ArgumentCaptor.forClass(String.class);
    verify(auditLogger, times(1)).log(logCaptor.capture());
    assertTrue(logCaptor.getValue().contains("idempotent skip:" + orderId));
}

// 4) 重试:首次 transient 错误失败,二次成功;断言 charge 调用两次、日志包含 retry、无 release
@Test
void process_retryOnTransientPaymentError_thenSuccess() {
    String orderId = "o-retry";
    List<LineItem> items = Arrays.asList(
            new LineItem("SKU-R1", 2, 2.5), // 2 * 2.5 = 5.0
            new LineItem("SKU-R2", 1, 5.0)  // 总计 10.0
    );
    Order order = new Order(orderId, items);

    when(orderRepository.isProcessed(orderId)).thenReturn(false);
    when(paymentGateway.charge(eq(orderId), anyDouble()))
            .thenReturn(new ChargeResult(false, true, null)) // 第一次:transient 失败
            .thenReturn(new ChargeResult(true, false, "TXN-2")); // 第二次:成功

    String result = processor.process(order);
    assertEquals("SUCCESS", result);

    // 支付两次,金额一致且为 10.0(两位小数)
    ArgumentCaptor<Double> amountCaptor = ArgumentCaptor.forClass(Double.class);
    verify(paymentGateway, times(2)).charge(eq(orderId), amountCaptor.capture());
    List<Double> amounts = amountCaptor.getAllValues();
    assertEquals(2, amounts.size());
    assertEquals(10.0, amounts.get(0), 1e-9);
    assertEquals(10.0, amounts.get(1), 1e-9);

    // 日志包含 retry 与 processed
    ArgumentCaptor<String> logCaptor = ArgumentCaptor.forClass(String.class);
    verify(auditLogger, atLeastOnce()).log(logCaptor.capture());
    List<String> logs = logCaptor.getAllValues();
    assertTrue(logs.stream().anyMatch(m -> m.contains("retry payment:" + orderId)));
    assertTrue(logs.stream().anyMatch(m -> m.contains("processed:" + orderId + ":txn=TXN-2")));

    // 不应释放库存
    verify(inventoryService, never()).release(anyString(), anyInt());
    // markProcessed 必须调用一次
    verify(orderRepository, times(1)).markProcessed(orderId);

    // 交互顺序:reserve 全部 -> 第一次 charge -> log retry -> 第二次 charge -> markProcessed -> log processed
    InOrder inOrder = inOrder(inventoryService, paymentGateway, orderRepository, auditLogger);
    inOrder.verify(inventoryService).reserve("SKU-R1", 2);
    inOrder.verify(inventoryService).reserve("SKU-R2", 1);
    inOrder.verify(paymentGateway).charge(eq(orderId), anyDouble());
    inOrder.verify(auditLogger).log("retry payment:" + orderId);
    inOrder.verify(paymentGateway).charge(eq(orderId), anyDouble());
    inOrder.verify(orderRepository).markProcessed(orderId);
    // processed 日志最后
    inOrder.verify(auditLogger).log(argThat(s -> s.contains("processed:" + orderId + ":txn=TXN-2")));
}

}

Maven 运行指引

  • 在 pom.xml 添加依赖:

    • junit-jupiter-api、junit-jupiter-engine(JUnit 5)
    • mockito-core、mockito-junit-jupiter(Mockito + JUnit5 扩展)
  • 示例依赖与插件: org.junit.jupiter junit-jupiter-api 5.10.2 test org.junit.jupiter junit-jupiter-engine 5.10.2 test org.mockito mockito-core 5.12.0 test org.mockito mockito-junit-jupiter 5.12.0 test org.apache.maven.plugins maven-surefire-plugin 3.2.5 false

  • 执行测试:

    • mvn test
    • 或仅运行本测试类:mvn -Dtest=app.order.OrderProcessorTest test

Gradle 运行指引(Kotlin 或 Groovy DSL 均可)

  • 依赖(Groovy DSL): dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0' } test { useJUnitPlatform() }

  • 执行测试:

    • ./gradlew test

说明

  • 测试覆盖正向、反向、边界与重试场景,严格验证:
    • reserve/charge/release/markProcessed/log 的调用次数与顺序(InOrder)。
    • 支付金额四舍五入到两位小数。
    • 失败时释放所有已预留库存且抛 PaymentException。
    • 幂等路径返回 ALREADY 且不触发库存与支付。
    • transient 错误重试一次成功,日志包含 retry,且不额外释放库存。
  • 使用 ArgumentCaptor 捕获并断言日志消息包含 txn 与 retry。

示例详情

解决的问题

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

适用用户

测试开发工程师

帮助测试开发人员快速生成复杂系统的单元测试,覆盖更多场景并确保代码的高质量交付。

全栈开发者

为全栈工程师在忙碌的开发任务中节省单元测试编写时间,为代码快速上线保驾护航。

技术管理者

为团队节省研发成本,同时提高测试覆盖率,保障产品质量,助力项目按时交付。

特征总结

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

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥10.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 142 tokens
- 6 个可调节参数
{ 编程语言 } { 代码片段 } { 测试场景说明 } { 依赖与模拟需求 } { 覆盖深度 } { 测试框架 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
限时免费

不要错过!

免费获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59