EF Core 测试体系详解:从 Specification 分层到 SQL 基线断言与自动重写机制
2026/9/14 17:10:34 网站建设 项目流程

EF Core 测试体系详解:从 Specification 分层到 SQL 基线断言与自动重写机制

【免费下载链接】efcoreEF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.项目地址: https://gitcode.com/GitHub_Trending/ef/efcore

EF Core 仓库的测试体系围绕"规格测试 + 提供程序实现测试"的分层架构构建,通过TestHelpers、共享数据库夹具和 SQL 基线断言三大基础设施,保证 LINQ 查询在多个数据库提供程序上行为一致且生成的 SQL 可被逐条验证。读完本文,你将理解 EF Core 测试类的继承层次与职责划分,掌握新增测试的标准工作流,并能用EF_TEST_REWRITE_BASELINES机制和dotnet exec直连测试程序集的方式高效处理 SQL 基线变更与单测隔离调试。

一、三类测试的职责划分

EF Core 的测试按隔离程度分为三类,目录结构即职责边界:

1. 单元测试(Unit Tests)

位于test/EFCore.Tests/test/EFCore.Relational.Tests/test/EFCore.{Provider}.Tests/(如test/EFCore.SqlServer.Tests/)。其特点是:

  • 测试孤立逻辑,不需要真实数据库
  • 通过*TestHelpers.Instance.CreateConventionBuilder()构建模型;
  • 通过CreateContextServices()解析出DbContext的内部服务容器,直接获取被测服务实例。

2. 规格测试(Specification Tests)

位于test/EFCore.Specification.Tests/(核心层)与test/EFCore.Relational.Specification.Tests/(关系层)。它们定义测什么——LINQ 查询表达式与预期查询结果,是抽象基类,不能直接运行:提供程序测试通过继承覆写,验证怎么测,即具体生成的 SQL 语句。

3. 功能测试(Functional Tests)

位于test/EFCore.{Provider}.FunctionalTests/(如test/EFCore.SqlServer.FunctionalTests/test/EFCore.Sqlite.FunctionalTests/test/EFCore.Cosmos.FunctionalTests/)。这些具体提供程序测试类继承对应的规格测试基类,其中绝大多数包含SQL 基线断言

二、测试类继承层次:以 Where 查询为例

以查询测试为例,继承链自底向上共四层:

QueryTestBase<TFixture> # Core(EFCore.Specification.Tests) └─ NorthwindWhereQueryTestBase<TFixture> # Specification(核心规格) └─ NorthwindWhereQueryRelationalTestBase<TFixture> # Relational specification(关系规格) └─ NorthwindWhereQuerySqlServerTest # Provider(提供程序,断言 SQL)

最末端的提供程序测试类采用统一的覆写模式:先调用基类跑完 LINQ 并断言结果,再断言本提供程序实际发出的 SQL。仓库中 NorthwindWhereQuerySqlServerTest.cs 的Where_simple即为该模式的典型实例:

public override async Task Where_simple(bool async) { await base.Where_simple(async); // 运行 LINQ 查询并断言结果集 AssertSql("""..."""); // 断言该提供程序生成的 SQL 基线 }

这种模式保证:基类改动结果断言时,所有提供程序自动受益;而 SQL 生成器变化时,只有对应提供程序的基线会失配,问题定位范围极小。

三、TestHelpers 体系:测试的服务与模型工厂

TestHelpers是各层测试获取依赖的核心入口,其继承结构为:

TestHelpers (abstract) # EFCore.Specification.Tests ├─ InMemoryTestHelpers # 非关系型提供程序 └─ RelationalTestHelpers (abstract) # EFCore.Relational.Specification.Tests ├─ SqlServerTestHelpers └─ SqliteTestHelpers

关键方法有三个:CreateConventionBuilder()CreateContextServices(model)CreateOptions()。从源码看(TestHelpers.cs):

  • CreateOptions(L14-L32):构造DbContextOptionsBuilder,注入内部IServiceProviderUseInternalServiceProvider),再调用抽象方法UseProviderOptions挂上提供程序特有的配置(连接串、方言等)。UseProviderOptions由各具体子类实现,这正是 SQL Server 与 Sqlite 的差异所在。
  • CreateServiceProvider(L34-L53):先调用抽象方法AddProviderServices(services)注册提供程序服务,再叠加调用方传入的customServices,最后BuildServiceProvider()构建容器(注释明确说明测试替身会违反作用域规则,因此关闭了 scope 校验)。
  • CreateConventionBuilder(L184-L210):组装模型构建所需的完整上下文——先应用提供程序选项,再解析出上下文服务,最后创建TestModelBuilder。它返回的TestModelBuilder(L416-L437)覆写了FinalizeModel(),额外暴露了FinalizeModel(designTime, skipValidation)重载,可分别拿到设计时模型与经过IModelRuntimeInitializer初始化后的运行时只读模型ReadOnlyModel注解),是模型约定(Convention)单元测试的关键工具。
  • AssertAllMethodsOverridden(L313-L361):这是"漏覆写检查"的实现——它反射收集测试类中所有声明于基类且带[Fact]/[Theory]/[ConditionalFact]/[ConditionalTheory]特性的方法,若当前类未覆写则断言失败,且错误信息会自动生成缺失的覆写方法骨架代码(含base调用与AssertSql()占位),开发者可直接粘贴使用。

RelationalTestHelpers.cs 进一步派生出SqlServerTestHelpersSqliteTestHelpers,为各自提供程序补齐连接与选项细节。

四、Fixture:共享存储与非共享模型两种夹具

SharedStoreFixtureBase :共享数据库夹具

定义于 SharedStoreFixtureBase.cs。适用于读多写少的测试(如 Northwind 查询测试)——大量测试共享同一个数据库实例。其InitializeAsync()(L47-L74)的关键流程:

  1. 通过TestStoreFactory.GetOrCreate(StoreName)获取(或按RecreateStore标记重建)测试存储;
  2. 注册服务:UsePooling为 true(默认)时调用AddPooledDbContextFactory注册连接池化的DbContextFactory,否则以 Transient 生命周期注册DbContext
  3. TestStore.InitializeAsync(...)完成建库建表、一次性种子数据填充SeedAsync)与清理逻辑(CleanAsync)的注册;
  4. 最后ListLoggerFactory.Clear()清空初始化期间记录的日志,保证后续测试的 SQL 断言从干净状态开始。

子类只需覆写SeedAsyncCleanStoreNameTestStoreFactory等成员。夹具还暴露ReseedAsync()(L95-L101)供测试在改动数据后重新清洗并回填种子。

NonSharedModelTestBase:每测试独立模型/存储

每个测试获得全新的模型与存储,测试内调用InitializeAsync<TContext>(onModelCreating, seed, ...)初始化。适用于需要独立 schema的测试——如模型构建、迁移、并发控制等会改变 schema 或数据的场景,避免共享库之间的相互污染。

五、SQL 基线断言与 Roslyn 自动重写

捕获机制

TestSqlLoggerFactory.cs(EFCore.Relational.Specification.Tests)派生出TestSqlLogger,其UnsafeLog(L315-L395)拦截RelationalEventId.CommandExecuted/CommandError/CommandExecuting事件,从结构化日志中提取commandTextparameters,将参数展开后的完整 SQL逐条存入SqlStatements列表;出错语句同时进入FailedSqlStatements,测试失败时会写入测试输出,便于定位导致数据库错误的语句。

断言机制

AssertSql("""...""")最终落到AssertBaseline(L63-L293):

  • assertOrder: true(默认)时逐条有序比对,并断言"没有多余的 SQL";
  • assertOrder: false时退化为包含断言,每个期望片段只需出现在实际语句中即可(用于事务/执行策略导致语句顺序不确定的场景);
  • forUpdate: true时为批量更新(ExecuteUpdate)场景,跳过首尾各一条语句。

基线失配时的两种救济

  1. 输出新基线:断言失败时,从调用栈反解析出调用方源文件名与行号,把实际 SQL 格式化为AssertSql("""...""")代码块打印到测试输出,供人工粘贴。语句超过 20 条时会标注 "Output truncated."。
  2. 自动重写源码:设置环境变量EF_TEST_REWRITE_BASELINES=1后,RewriteSourceWithNewBaseline(L128-L292)直接使用Roslyn解析源文件(CSharpSyntaxTree.ParseText),定位到该AssertSql(...)调用表达式节点,用新基线原地替换整段调用,其余内容逐字节拷贝。实现上有几个精巧细节:
    • ConcurrentDictionary<string, QueryBaselineRewritingFileInfo>按文件加锁,避免并发重写冲突;
    • ProcessedLines记录已处理的行(同一 Theory 多次执行时防重复);
    • LineDisplacements记录此前重写造成的行号偏移,修正后续错误的行号定位;
    • 检测并保留原文件的 UTF-8 BOM,统一转换为 Unix 换行。

同一机制还用于两处:Cosmos 提供程序的 TestSqlLoggerFactory.cs(Cosmos 查询基线),以及 CompiledModelTestBase.cs 中编译模型基线(compiled model baseline)的重写。

六、工作流:新增一个测试的完整步骤

  1. 规格测试:在EFCore.Specification.Tests(核心层)或EFCore.Relational.Specification.Tests(关系层)中新增测试方法,定义 LINQ 查询与结果断言;
  2. 提供程序覆写:在每一个继承该基类的提供程序功能测试类(EFCore.{Provider}.FunctionalTests)中覆写新方法,加上符合该提供程序的AssertSql基线断言;
  3. 单元测试:如涉及孤立逻辑,在EFCore.{Provider}.Tests中补充单元测试;
  4. 生成初始基线:以EF_TEST_REWRITE_BASELINES=1运行测试,让 Roslyn 自动把各提供程序的实际 SQL 写入源码;
  5. 禁用--no-build:确保项目重建、代码改动被测试程序集拾取;
  6. 跨平台验证:涉及文件路径、路径分隔符等跨平台代码时,在 Windows 与 Linux/macOS 上分别验证。

七、常见陷阱与对策

陷阱对策
基线失配(SQL 基线或编译模型基线)EF_TEST_REWRITE_BASELINES=1重新运行,自动重写源码
Check_all_tests_overridden失败所有继承该基类的提供程序测试类中覆写新测试方法(错误信息会附赠可粘贴的覆写骨架,见AssertAllMethodsOverridden
某 SQL Server 特性在较低兼容级别下不可用[SqlServerCondition(...)]条件特性门控该测试

八、直接对已构建程序集运行单个测试

当需要绕开dotnet test的完整重建、只隔离运行某个测试方法时,可用dotnet exec直连测试 DLL,但必须附加--filter-not-trait category=failing --ignore-exit-code 8

dotnet exec ./Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.dll ` --filter-method '*MigrationsSqlServerTest.Create_json_index_over_whole_complex_collection' ` --filter-not-trait category=failing --ignore-exit-code 8

这两个标志与 test/Directory.Build.props 中经TestingPlatformCommandLineArguments传给 Microsoft.Testing.Platform 的参数保持一致:category=failing标记了当前已知失败的测试,过滤掉它们才能得到与dotnet test一致的结果;--ignore-exit-code 8则让测试平台在特定退出码下仍视为可解析运行。注意:除非测试程序集在紧前一步刚构建完成,否则不要附加--no-build,否则运行的将是过期的程序集,基线对比毫无意义。

小结

EF Core 的测试架构用"规格层定义语义、提供程序层锚定 SQL"的双层契约,把庞大的多提供程序矩阵测试分解为可独立验证的单元:TestHelpers提供模型与服务工厂,SharedStoreFixtureBase摊销了共享库的初始化成本,而基于 Roslyn 的基线自动重写则把"SQL 变了"这类最频繁的维护成本压缩到了一次环境变量开关的距离。理解这套机制后,无论是阅读 EF Core 的测试代码,还是为其他 ORM 设计测试体系,都能从中获得可直接借鉴的分层与自动化思路。

【免费下载链接】efcoreEF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.项目地址: https://gitcode.com/GitHub_Trending/ef/efcore

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询