CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
ASP.NET Core 实战入门/课时/测试与 E2E
RawBlame
课时目录12 节

ASP.NET Core 实战入门

  1. 01ASP.NET Core 基础
  2. 02C# 与 .NET 基础
  3. 03Entity Framework Core
  4. 04依赖注入
  5. 05REST / GraphQL / gRPC
  6. 06数据存储与缓存
  7. 07SignalR 实时通信
  8. 08测试与 E2E
  9. 09消息队列
  10. 10微服务与网关
  11. 11日志、任务与工具链
  12. 12综合练习

测试与 E2E

xUnit、Moq、Testcontainers、Playwright。

35 分钟

测试策略:xUnit 与集成测试(对照 JUnit/Mockito)

这一课你将能做到

  1. 用 xUnit 写出参数化的单元测试,覆盖判题服务的各分支(AC/WA/TLE/RE)
  2. 用 WebApplicationFactory 起真实宿主做集成测试,DI 覆盖为内存数据库
  3. 对照 JUnit 5/Mockito/@SpringBootTest 建立完整的测试金字塔与命令行流程

为什么要学

测试是「敢不敢重构」的唯一底气。Java 侧的组合 JUnit 5 + Mockito + @SpringBootTest(+ Testcontainers),.NET 侧对应 xUnit + NSubstitute + WebApplicationFactory,连「单元测业务、集成测管线」的分层策略都相同。

人话:单元测试把类拎出来验逻辑(依赖换假货),集成测试把应用真起来发 HTTP 请求验装配。判题服务规则多、边界多(超时 1ms 之差翻转 AC/TLE),最值得堆测试。

先建立图像

   集成测试(少) WebApplicationFactory ≙ @SpringBootTest + Testcontainers
   ────────────  真实路由/绑定/DI,数据库用替身
   单元测试(多) xUnit + NSubstitute    ≙ JUnit 5 + Mockito

跟着做

步骤 1:建测试项目

dotnet new xunit -o Oj.Api.Tests
cd Oj.Api.Tests
dotnet add reference ../Oj.Api/Oj.Api.csproj
dotnet add package NSubstitute
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet test

步骤 2:单元测试——判题规则(xUnit 参数化)

被测代码:根据用例结果与资源占用判定状态:

public class VerdictEvaluator
{
    public static string Evaluate(bool outputMatched, int timeMs, int limitMs, int exitCode) =>
        (outputMatched, timeMs, limitMs, exitCode) switch
        {
            (_, _, _, != 0)   => "RE",
            (false, _, _, 0)  => "WA",
            (true, > 0, _, 0) when timeMs > limitMs => "TLE",
            (true, _, _, 0)   => "AC",
            _                 => "SE",
        };
}

测试类([Theory] + [InlineData] = JUnit 的 @ParameterizedTest + @CsvSource):

public class VerdictEvaluatorTests
{
    [Theory]
    [InlineData(true,  100, 1000, 0, "AC")]    // 输出对、没超时
    [InlineData(true,  2000, 1000, 0, "TLE")]  // 超时
    [InlineData(false,  50, 1000, 0, "WA")]    // 答案错
    [InlineData(true,    0, 1000, 1, "RE")]    // 非零退出码优先判 RE
    public void Evaluate_Should_Classify(bool matched, int time, int limit, int code, string expected)
    {
        var actual = VerdictEvaluator.Evaluate(matched, time, limit, code);
        Assert.Equal(expected, actual);
    }
}

断言对照:Assert.Equal(a, b) ≙ ; ≙ 。

assertEquals
Assert.ThrowsAsync<T>
assertThrows

步骤 3:Mock 依赖(NSubstitute,Mockito 平替)

public interface ISandboxRunner { Task<RunOutcome> RunAsync(string code, string lang, CancellationToken ct); }

public class JudgeService(ISandboxRunner runner, IVerdictStore store)
{
    public async Task<string> JudgeAsync(string code, string lang, CancellationToken ct)
    {
        var outcome = await runner.RunAsync(code, lang, ct);
        var verdict = VerdictEvaluator.Evaluate(outcome.Matched, outcome.TimeMs, 1000, outcome.ExitCode);
        await store.SaveAsync(code, verdict, ct);
        return verdict;
    }
}

[Fact]
public async Task JudgeAsync_Should_Persist_Verdict()
{
    var runner = Substitute.For<ISandboxRunner>();                 // 创建假实现
    runner.RunAsync("int main(){}", "cpp", Arg.Any<CancellationToken>())
          .Returns(new RunOutcome(false, 120, 0));                 // 打桩 ≙ when(...).thenReturn(...)
    var store = Substitute.For<IVerdictStore>();

    var verdict = await new JudgeService(runner, store).JudgeAsync("int main(){}", "cpp", default);

    Assert.Equal("WA", verdict);
    await store.Received(1).SaveAsync("int main(){}", "WA", Arg.Any<CancellationToken>()); // ≙ verify
}

语法对照:Substitute.For<T>() ≙ mock(T.class);.Returns(...) ≙ when(...).thenReturn(...);Received(1) ≙ verify(mock, times(1))。

步骤 4:集成测试——WebApplicationFactory 起真应用

public class OjApiFactory : WebApplicationFactory<Program>
{
    private readonly SqliteConnection _conn = new("DataSource=:memory:");

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        _conn.Open();   // 连接开着内存库才活着
        builder.ConfigureServices(services =>
        {
            services.RemoveAll<DbContextOptions<OjDbContext>>();     // 移除真库,换共享内存 SQLite
            services.AddDbContext<OjDbContext>(o => o.UseSqlite(_conn));
            using var scope = services.BuildServiceProvider().CreateScope();
            scope.ServiceProvider.GetRequiredService<OjDbContext>().Database.EnsureCreated();
        });
    }
}

注意:顶级语句项目的 Program 对测试可见,需在 Program.cs 末尾加 public partial class Program { }。

步骤 5:发真实 HTTP 请求验证 API 契约

public class SubmissionsApiTests(OjApiFactory factory) : IClassFixture<OjApiFactory>   // ≙ @SpringBootTest(webEnvironment)
{
    private readonly HttpClient _client = factory.CreateClient();

    [Fact]
    public async Task Submit_Should_Return_201_With_Location()
    {
        var resp = await _client.PostAsJsonAsync("/api/submissions",
            new { problemId = 1, language = "cpp", code = "int main(){}" });

        Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
        Assert.NotNull(resp.Headers.Location);
        var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal("PENDING", body.GetProperty("status").GetString());
    }

    [Fact]
    public async Task Submit_With_Empty_Code_Should_Return_400()
    {
        var resp = await _client.PostAsJsonAsync("/api/submissions",
            new { problemId = 1, language = "cpp", code = "" });
        Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);   // 校验管线真实生效
    }
}

IClassFixture 让工厂跨测试类复用(宿主只启动一次),等价 @SpringBootTest 单上下文复用。

步骤 6:外部依赖真实化(Testcontainers)

SQLite 与真实数据库的方言/约束有偏差,关键集成测试上真容器:

dotnet add package Testcontainers.PostgreSql
var pg = new PostgreSqlBuilder().Build();
await pg.StartAsync();
services.AddDbContext<OjDbContext>(o => o.UseNpgsql(pg.GetConnectionString()));

对应 Java 的 Testcontainers 同名生态。

讲清楚要点

  1. 金字塔策略两边完全一致:大多数是纯单元(毫秒级、无框架),少量集成验路由/绑定/校验/DI 装配,更少端到端。判题平台的分配:VerdictEvaluator/分页/DTO 全单元;提交 API、鉴权、SignalR 握手做集成。
  2. xUnit 测试类每次测试都重新实例化(保证测试间无共享状态):构造函数 = @BeforeEach,Dispose = @AfterEach,异步初始化用 IAsyncLifetime。
  3. dotnet test 一条命令出结果:CI 里加 --collect:"XPlat Code Coverage" 出覆盖率,--filter 按名过滤,等价 mvn test -Dtest=...。
  4. 集成测试里覆盖 DI 是关键技巧:ConfigureServices 里 RemoveAll<T> 后加测试替身——等价 @MockBean,把判题沙箱替换成假实现即可全链路跑通。

你可能会懵的地方

  • [Fact] 和 [Theory] 怎么选? 无参用 [Fact];同逻辑多数据集用 [Theory] + [InlineData]。看到重复结构的测试方法就合并成 Theory。

  • 单元测试要不要 mock DbContext? 不要——mock 掉 EF 后测的是 mock 自己。单元测只测纯逻辑(如 VerdictEvaluator),数据相关挪到集成测试用 SQLite/容器验,与 Java 社区「不 mock Repository」的共识一致。

  • 测试顺序、测试依赖怎么控制? xUnit 默认类间并行、类内串行且无顺序保证——这是特性不是缺陷。需要顺序说明存在共享状态,优先重构测试;确需串行用 [Collection] 分组。

  • 怎么测 SignalR 推送? 用 SignalR 客户端库连到 TestServer 的 /hubs/judge,订阅事件后触发业务,断言收到消息——握手与序列化都被真实验证。

  • 私密配置(连接串)在测试里怎么办? 测试项目的环境变量注入,CI 用 secrets,绝不让生产连接串进仓库。

错法对照

错法问题正解
集成测试直接连开发库污染数据、跑两次结果不一致每测试类独立 SQLite 内存库/容器
mock 了被测对象自己测试没有意义只 mock 边界依赖(沙箱、网关),测纯逻辑
断言只有「不抛异常」什么都没验证断言返回值/状态码/副作用调用
测试里 sleep 等异步完成又慢又不稳定显式 await,SignalR 用事件等待
一个测试方法验证十件事首个失败掩盖后续一个行为一个测试,Theory 拆数据集

卡点急救

  1. 集成测试报「Program 类不可访问」:在 Program.cs 文件末尾加 public partial class Program { }。
  2. SQLite 内存库表不存在:连接必须保持打开,EnsureCreated 在首次使用前执行;连接关闭即库消失。
  3. 测试并行导致日志混乱/端口冲突:WebApplicationFactory 用内存 TestServer 不占端口;日志用 ITestOutputHelper 注入。

面试常问

  • 单元测试和集成测试的边界怎么划分? 单元测试验证「一个类的业务规则」,依赖全替换,毫秒级;集成测试验证「多组件装配正确性」(路由→绑定→校验→DI→序列化),数据库用替身或容器。判别标准:失败修复点在一个类里是单元,跨配置/跨组件是集成。

  • .NET 侧 @MockBean 的等价物? ConfigureWebHost 里 RemoveAll<T>() 后 AddSingleton(mock实例),替换任意已注册服务,配合 Received() 断言副作用。

  • 如何让测试跑得又快又稳? 工厂/容器跨测试复用(IClassFixture)、显式 await 替代 sleep、无共享文件/端口的并行隔离、常用路径用内存库、方言差异场景才上 Testcontainers。

自测三问

  1. [Fact] 与 [Theory] 的区别?
  2. 为什么不建议 mock DbSet?
  3. WebApplicationFactory 默认监听真实端口吗?请求发到哪里?

小结一句话

xUnit + NSubstitute + WebApplicationFactory = JUnit + Mockito + @SpringBootTest 的同构映射,测试金字塔的分层策略原封不动,判题规则这种多分支模块值得最高单测密度。

延伸

  • 官方文档:ASP.NET Core 集成测试
  • 下一课:异步消息与事件驱动——判题队列削峰、重试与幂等,对照 Spring AMQP。
加载本节练习…
← 上一课SignalR 实时通信下一课 →消息队列