Stop writing a test-data builder for every class in .NET
Stop writing a test-data builder for every class in .NET
停止为 .NET 中的每个类编写测试数据构建器
If you’ve ever written test data by hand, you know the ritual: a PersonBuilder, an OrderBuilder, an AddressBuilder… one hand-written builder per class, each one a wall of WithX(...) methods you have to maintain forever. The Test Data Builder and Object Mother patterns are great — the boilerplate is not.
如果你曾经手动编写过测试数据,你一定熟悉这个流程:PersonBuilder、OrderBuilder、AddressBuilder……每个类都要写一个手写的构建器,每一个都充斥着你需要永远维护的 WithX(...) 方法。Test Data Builder 和 Object Mother 模式固然很好,但那些样板代码却不然。
XModelBuilder gives you a fluent builder for any C# class out of the box. No per-class builder required. It handles constructor parameters, init-only properties, read-only members, even private backing fields — via reflection, deterministically.
XModelBuilder 为任何 C# 类提供了开箱即用的流式构建器,无需为每个类单独编写构建器。它通过反射确定性地处理构造函数参数、init-only 属性、只读成员,甚至是私有后备字段。
30-second example
30 秒示例
You can use it fully standalone (no DI container) through a small static facade: 你可以通过一个小型静态外观类完全独立地(无需依赖注入容器)使用它:
using XModelBuilder.Default;
var order = For.Model<Order>()
.With(x => x.OrderDate, new DateTime(2026, 7, 1))
.With(x => x.Lines[0].Product, "Widget") // deep paths + indexers just work
.With(x => x.Lines[0].Quantity, 3)
.Build();
No OrderBuilder, no OrderLineBuilder. The Lines[0].Product path drills into a nested collection element and sets it for you. Need a whole list? Create.Models<Order>(10).
无需 OrderBuilder,也无需 OrderLineBuilder。Lines[0].Product 路径会自动深入嵌套的集合元素并进行设置。需要整个列表?使用 Create.Models<Order>(10) 即可。
Deterministic fakers, seeded once
确定性的伪造数据(Fakers),一次性设置种子
Random test data that changes every run is a debugging nightmare. XModelBuilder ships a seeded, dependency-free faker (and a Bogus integration if you prefer). Register it once:
每次运行都会改变的随机测试数据是调试的噩梦。XModelBuilder 提供了一个带种子、无依赖的伪造器(如果你喜欢,也可以集成 Bogus)。只需注册一次:
services.AddXModelBuilder()
.AddXFaker(seed: 12345); // reproducible values, every run
Then let it fill in the noise while you set only what your test actually cares about: 然后让它填充无关紧要的数据,而你只需设置测试真正关心的部分:
var order = xprovider.For<Order>()
.With(x => x.Id, p => p.XFake().NewGuid())
.With(x => x.Customer.Name, p => p.Bogus().Company.CompanyName())
.With(x => x.Lines[0].Quantity, 3)
.Build();
XFake().NewGuid("customer-acme") even gives you a stable GUID from a name — same key, same GUID, regardless of call order or parallelism. Deterministic by design.
XFake().NewGuid("customer-acme") 甚至可以根据名称生成稳定的 GUID —— 无论调用顺序或并行度如何,相同的键总是生成相同的 GUID。设计上保证了确定性。
Build a whole list: BuildMany
构建整个列表:BuildMany
Need ten of something, each slightly different? BuildMany reuses one builder and re-evaluates the varying parts on every instance, while keeping the shared bits fixed:
需要十个对象,且每个都略有不同?BuildMany 会重用一个构建器,并在每个实例上重新评估变化的部分,同时保持共享部分固定不变:
var people = xprovider.For<Person>()
.With(p => p.City, "Amsterdam") // shared by all 10
.With(p => p.Name, p => p.Bogus().Name.FullName()) // 10 different names
.BuildMany(10);
Prefer to vary explicitly by index? There’s an overload for that: 更喜欢通过索引显式地进行变化?可以使用重载方法:
var people = xprovider.BuildMany<Person>(5, (b, i) => b
.With(p => p.Name, $"Person{i}"));
Standalone (no DI) it’s a single call: Create.Models<Person>(10).
在独立模式下(无 DI),只需一次调用:Create.Models<Person>(10)。
Give a class sensible defaults (optional)
为类提供合理的默认值(可选)
When you do want per-type defaults, you write a tiny builder — and only the defaults, nothing else: 当你确实需要针对特定类型的默认值时,只需编写一个微小的构建器,且仅包含默认值,无需其他内容:
[ModelBuilder("person")]
public sealed class PersonBuilder(
IOptions<ModelBuilderOptions> options,
IModelBuilderProvider xprovider) : ModelBuilder<PersonBuilder, Person>(options, xprovider)
{
protected override void SetDefaults() => WithDefault(p => p.Address); // Address fills itself
}
There’s no framework-wide “build the whole graph” recursion to fight: each type fills its own level, and a back-reference is simply a default you don’t write. No cycle guards needed. 无需处理框架级的“构建整个图”递归问题:每个类型填充自己的层级,反向引用只是一个你无需编写的默认值。无需循环保护。
More than one recipe per type: named builders
每个类型支持多种配方:命名构建器
Sometimes you want several presets for the same class — a minimal one and a fully-populated one, say. Give each builder a unique name via [ModelBuilder("...")]:
有时你可能需要同一个类的多个预设——比如一个最小化版本和一个完整填充版本。通过 [ModelBuilder("...")] 为每个构建器指定一个唯一名称:
[ModelBuilder("complex-address")]
public sealed class ComplexAddressBuilder(
IOptions<ModelBuilderOptions> options,
IModelBuilderProvider xprovider) : ModelBuilder<ComplexAddressBuilder, Address>(options, xprovider)
{
protected override void SetDefaults() => With(x => x.Street, "Main Street");
}
Register as many as you like and designate which one is the default (no magic strings — the model type is derived from the builder): 你可以注册任意数量的构建器,并指定哪一个是默认的(没有魔术字符串——模型类型是从构建器中推导出来的):
services
.AddModelBuilder<ComplexAddressBuilder>() // [ModelBuilder("complex-address")]
.AddModelBuilder<SimpleAddressBuilder>() // [ModelBuilder("simple")]
.UseAsDefaultModelBuilder<SimpleAddressBuilder>();
For<Address>() now uses the default. In C# you’ll normally reach for a specific builder in a typed way — compiler-checked, refactor-safe, no magic string:
现在 For<Address>() 会使用默认值。在 C# 中,你通常会以类型安全的方式调用特定的构建器——经过编译器检查、重构安全,且没有魔术字符串:
var fancy = xprovider.Use<ComplexAddressBuilder>().Build();
var five = xprovider.Use<ComplexAddressBuilder>().BuildMany(5);
The string name (“complex-address”) is really there for the places where you have no type at hand — first and foremost Gherkin tables and the mini data language. In a table, a cell for an Address member simply reads complex-address, and because the target is a reference type it resolves to that named builder automatically:
字符串名称(“complex-address”)主要用于无法直接获取类型的地方——最主要的是 Gherkin 表格和微型数据语言。在表格中,Address 成员的单元格只需填写 complex-address,由于目标是引用类型,它会自动解析为该命名构建器:
| Customer.Name | ShippingAddress |
|---|---|
| Jane Smith | complex-address |
(Need the literal text instead of a builder? Escape it: @complex-address.) That’s the theme of the whole untyped layer: faker tokens, deep-path strings and named-builder references exist to make text-driven sources like Gherkin first-class. In plain C# you stay strongly typed; in a feature file you get the same power from plain text.
(如果需要字面文本而不是构建器?使用转义:@complex-address。)这就是整个非类型化层的核心主题:伪造令牌、深层路径字符串和命名构建器引用,旨在使 Gherkin 等文本驱动的源成为一等公民。在纯 C# 中,你保持强类型;而在功能文件中,你可以通过纯文本获得同样强大的功能。
Gherkin tables become objects
Gherkin 表格转换为对象
This is where it clicks for BDD. A Reqnroll/SpecFlow table maps straight onto a model — dot-paths, indexers, type conversion and faker tokens all work inside the table: 这就是 BDD 的精髓所在。Reqnroll/SpecFlow 表格可以直接映射到模型——点路径、索引器、类型转换和伪造令牌在表格内均可使用:
Given the following order:
| Id | Customer.Name | Lines[0].Product | Lines[0].Quantity |
| xfake.NewGuid() | Jane Smith | Widget | 3 |
[Given("the following order:")]
public void GivenTheFollowingOrder(Table table) =>
_order = _xprovider.For<Order>().CreateModel(table);
Your feature file is your test data. It auto-detects the two common table shapes (vertical Field | Value and horizontal), and the column headers for the vertical shape are configurable per language. 你的功能文件就是你的测试数据。它会自动检测两种常见的表格形状(垂直的 Field | Value 和水平的),并且垂直形状的列标题可以按语言进行配置。
Why I like it
为什么我喜欢它
- One generic base class builds every model — no hand-written builders required. 一个通用的基类即可构建所有模型——无需手写构建器。
- Deterministic: seeded fakers, name-based stable GUIDs, TimeProvider-driven dates. 确定性:带种子的伪造器、基于名称的稳定 GUID、由 TimeProvider 驱动的日期。
- Works with
Microsoft.Extensions.DependencyInjectionor fully standalone. 可与Microsoft.Extensions.DependencyInjection配合使用,也可完全独立运行。 - A mini data language turns plain strings into arrays, dictionaries and nested objects. 微型数据语言可将纯字符串转换为数组、字典和嵌套对象。
- If you write a lot of tests, it removes a whole category of maintenance. 如果你编写大量测试,它将消除一整类维护工作。
GitHub: https://github.com/jl