FEATURED · 精选文章

C#/.NET测试驱动开发实战:从工具选型到可测试性设计

发布时间 / 2026/9/9 8:06:30
来源 / 创域科博编辑部
栏目 / 资讯中心
C#/.NET测试驱动开发实战:从工具选型到可测试性设计 在C#和.NET这个生态里聊测试驱动开发很容易落入一个尴尬局面工具书和文档看了不少红灯-绿灯-重构几个词背得滚瓜烂熟可真到了自己写业务代码时还是觉得测试无从下手。尤其当项目里既有Web API、又有WinForm上位机还要处理串口数据、后台线程任务的时候写测试这件事本身就变成了一个不小的工程。这篇是C#和.NET测试驱动开发实用指南的第三篇前两篇我们聊了TDD的基本概念和测试框架入门这篇我打算完全站在实战角度把我在真实项目里做TDD的完整套路、工具选择、依赖设计以及踩过的坑一次性摊开来讲。内容偏经验向适合已经跑通过简单测试用例、想在真实项目里稳定落地TDD的C#/.NET开发者。我要强调一下这篇不是理论复习而是我在项目里到底怎么用TDD的记录。里面的每一个结论几乎都是拿真实项目的代码和线上故障换来的。你会看到我为什么把主力测试框架从NUnit换成了xUnit为什么Mock库从Moq切到了NSubstitute也会看到一个完整的库存扣减服务是怎么从失败测试一路走到可运行代码的。如果你正卡在测试写了但没感觉代码根本没法测这个阶段这篇应该能给你一些能直接用的思路。1. 工具链选型先把手里的工具磨利写TDD的第一步不是写测试而是选对工具。.NET生态里的测试框架和测试库非常多选错了后面会很难受。我自己的经历是从NUnit用到xUnitMock库从Moq换到NSubstitute断言库最后稳定在FluentAssertions。下面把这几轮选型的思路和理由说清楚。1.1 测试框架横向对比xUnit、NUnit、MSTest怎么选这三家是目前C#/.NET项目里最常见的测试框架。很多人纠结选哪个其实它们的功能重叠度非常高真正的差异在使用体验和生态配套上。我用一个表格把核心差异列出来方便你对照自己的项目情况选型。维度xUnitNUnitMSTest并行执行原生支持默认按测试集合并行需要显式配置并行级别需要额外配置断言风格Assert.Equal等简洁方法Assert.That表达式风格非常丰富Assert.AreEqual等传统风格数据驱动测试通过MemberData/ClassData/InlineData通过TestCase/TestCaseSource通过DataRow/DynamicData扩展性有xUnit扩展机制可自定义FactAttribute等有自定义Attribute机制相对封闭项目生态.NET官方新模板的默认选择社区资料最活跃老牌框架遗留项目占比较高跟随Visual Studio开箱即用我的建议很简单新项目直接用xUnitVisual Studio模板默认就是它社区内容最多遇到问题最容易搜到答案。老项目如果已经用NUnit且运行稳定不要轻易迁移除非团队确实有强烈痛点。MSTest我个人不太推荐用于新项目它的定位更偏向Visual Studio开箱即用的场景在灵活性和扩展性上跟xUnit有差距。选框架还有个容易被忽略的点团队稳定性。工具再强如果团队用不顺手最后也会退化成一堆没维护的测试代码。所以选哪个不是看谁功能最强而是看谁能稳定地用三年以上。xUnit在当前环境下是最不容易出错的答案。1.2 模拟库的选择Moq、NSubstitute还是FakeItEasyMock库是TDD里绕不开的话题。写单元测试时外部依赖比如数据库、HTTP接口、文件系统都需要用模拟对象替身来控制行为和验证交互。我用过Moq很久现在主力是NSubstitute原因很简单它读起来更像人话。同样是模拟一个仓储接口的扣减方法Moq的写法是这样的var repo new MockIInventoryRepository(); repo.Setup(r r.TryDeductAsync(It.IsAnystring(), It.IsAnyint())) .ReturnsAsync(true);NSubstitute的写法是这样的var repo Substitute.ForIInventoryRepository(); repo.TryDeductAsync(Arg.Anystring(), Arg.Anyint()) .Returns(true);一眼看过去NSubstitute几乎不需要解释。对于团队里刚接触测试的同学这种直觉化的API能大幅降低学习成本。Moq的问题是Setup链式写法在复杂场景下嵌套太深读起来很费劲。FakeItEasy也值得一提它的风格介于两者之间A.FakeIInventoryRepository()和A.CallTo(() repo.Deduct(...))的写法也有不少忠实用户。选型逻辑跟测试框架一样新团队用NSubstitute老团队维持现状不折腾。不过我要泼一盆冷水Mock不是万能的。如果一个类的测试需要Mock七八个依赖大概率是这个类违反了单一职责原则。Mock用得越多越要回头审视设计。后面第3部分会专门讲怎么通过依赖注入让测试替身更自然从而少Mock、多断言真实行为。注意无论是Moq还是NSubstitute都不要把模拟对象当作到处都能贴的膏药。过度Mock会让测试变成测试自己的实现完全失去保护网的作用。1.3 断言风格与覆盖率别被数字绑架断言库推荐FluentAssertions最大优势是失败信息非常友好。比如两个集合比较失败时原生Assert会告诉你断言失败而FluentAssertions会告诉你预期包含元素X但实际缺少X多余Y定位问题能省一半时间。using FluentAssertions; // 原生断言失败信息Assert.Equal failed. // FluentAssertions失败信息Expected collection to contain item SKU001 but found [SKU002, SKU003]. result.Should().BeTrue(); inventory.RemainingQuantity.Should().Be(5);覆盖率这块我明确反对把覆盖率100%当目标。覆盖率是结果不是过程。我看过太多团队为了达标写出一堆只验证方法没抛异常的空测试这种测试除了让数字好看没有任何保护作用。我的经验是核心业务逻辑覆盖率80%以上外围代码比如Controller、ViewModel靠集成测试和手动验证兜底就够。比例不是绝对的关键是测试要断言真实行为而不是凑行数。2. TDD核心节奏从失败测试到可运行代码工具选好之后接下来是TDD最核心的东西红灯-绿灯-重构的节奏。很多人误以为TDD就是先写测试再写代码其实它的重点不是顺序而是通过测试来驱动设计。你写测试之前必须先想清楚接口长什么样、行为边界在哪、异常如何处理——这一步本身就是在做设计。2.1 需求拆解把一句话需求变成行为清单拿一个具体的例子来演示。假设要开发一个库存扣减服务需求描述只有一句话调用扣减接口库存够就成功库存不够就失败。这句话太模糊了直接开写很容易写出一个充满了if-else、边界模糊的方法。TDD逼着你先把行为清单列出来库存充足时扣减成功返回true库存不足时返回false且库存不能变扣减数量为0或负数时抛参数异常扣减成功时必须调用仓储的持久化方法商品不存在时怎么处理并发扣减时怎么保证不超卖这个清单就是测试清单也是你后面写实现代码的验收标准。注意清单不是写测试时才有的它是你从需求到代码之间的桥梁。需求方说库存不够就失败你得追问失败是返回false还是抛异常失败之后库存怎么处理这些问题的答案最后都要落到测试里。2.2 红灯阶段先写一个必然会失败的测试红灯阶段的要点是测试必须真实不能先写实现再回头凑测试。以库存充足时扣减成功这个行为为例先写测试public class InventoryServiceTests { [Fact] public async Task DeductAsync_WhenStockIsSufficient_ShouldReturnTrue() { var repo Substitute.ForIInventoryRepository(); repo.GetStockAsync(SKU001) .Returns(new StockItem(SKU001, 10)); var service new InventoryService(repo); var result await service.DeductAsync(SKU001, 3); result.Should().BeTrue(); await repo.Received(1).SaveStockAsync(Arg.IsStockItem(s s.Quantity 7)); } }这个测试很明确地表达了行为期望扣减之后能查到剩余7件并且调用了一次保存。此时InventoryService类还不存在编译都过不了——这就是红。这里有个小技巧测试里的命名要像一句完整的话比如库存充足时扣减返回真这样测试失败时你能从名字直接读出哪里出了问题。2.3 绿灯阶段用最少代码让测试通过红灯之后的目标是尽快变绿但不代表可以随便写。绿灯阶段的原则是能过就行别多想。拿上面的测试最少的实现可以是public class InventoryService(IInventoryRepository repo) { public async Taskbool DeductAsync(string sku, int quantity) { var stock await repo.GetStockAsync(sku); if (stock.Quantity quantity) { return false; } stock.Quantity - quantity; await repo.SaveStockAsync(stock); return true; } }注意我用了C# 12的主构造函数写法这在现代.NET项目里已经非常常见。这个实现能通过第一个测试但还不够健壮——数量为负数、库存不足、并发扣减等场景都没覆盖。别着急那是后面一组测试的事。你现在只需要让当前的红灯变绿。2.4 重构阶段在绿灯的保护下调整结构绿灯之后是重构。这一步有测试兜底你可以放心大胆地调整代码内部结构只要保证测试依然通过。比如把库存不足时返回false这个分支逻辑提取为一个单独的方法让主流程更清晰public async Taskbool DeductAsync(string sku, int quantity) { var stock await repo.GetStockAsync(sku); if (!HasEnoughStock(stock, quantity)) { return false; } DeductStock(stock, quantity); await repo.SaveStockAsync(stock); return true; } private static bool HasEnoughStock(StockItem stock, int quantity) stock.Quantity quantity; private static void DeductStock(StockItem stock, int quantity) stock.Quantity - quantity;重构的原则是每次只做一个小改动改完立刻跑测试。很多初学者容易在这里翻车——重构时野心太大顺手把功能也改了结果测试全红还分不清是重构的锅还是新功能的锅。记住重构阶段严禁添加新行为只改结构。3. 依赖注入与可测试性设计让测试替身更自然一个类能不能被轻松测试很大程度上取决于它的依赖是怎么被拿到的。我见过太多不可测代码问题都出在依赖藏在方法内部直接new一个SqlConnection、直接调用DateTime.Now、到处访问静态设置类。TDD做不下去十有八九是设计问题而不是测试技巧问题。3.1 构造函数注入是默认首选依赖注入有三种常见方式构造函数注入、属性注入、方法参数注入。在C#/.NET里我几乎只用构造函数注入。构造函数注入口味有点老土但它是唯一一种能在测试时强制你提供依赖的方式——测试代码里new对象时编译器就逼着你把每个依赖传进去。public class OrderService { private readonly IInventoryRepository _repo; private readonly IPaymentGateway _payment; public OrderService(IInventoryRepository repo, IPaymentGateway payment) { _repo repo; _payment payment; } public async Taskbool PlaceOrderAsync(Order order) { // 业务逻辑... } }测试时var service new OrderService( Substitute.ForIInventoryRepository(), Substitute.ForIPaymentGateway());属性注入看起来省事但测试时很容易忘记设置某个依赖导致运行时才爆NullReferenceException。方法参数注入适用于依赖只在单个方法里用的情况比如把一个IClock传给某个需要时间判断的方法但这样会让方法签名变得很啰嗦。我的原则是绝大部分情况用构造函数注入极少数临时依赖用方法参数注入属性注入能不碰就不碰。3.2 时间、配置、随机数这些隐式依赖怎么处理C#项目里最容易让测试翻车的是那些看起来不需要注入的隐式依赖。首当其冲的就是时间。假设有一个判断优惠券是否过期的逻辑public bool IsCouponValid(Coupon coupon) { return coupon.ExpiryDate DateTime.Now; }这个代码在单元测试里没法测你没法控制DateTime.Now到底是哪一秒。解决方案是把时间抽象出来。.NET 8里微软官方引入了TimeProvider抽象类内置了TimeProvider.System实例测试时用FakeTimeProvider手动控制当前时间这是目前最推荐的做法。public class CouponService(TimeProvider timeProvider) { public bool IsCouponValid(Coupon coupon) { return coupon.ExpiryDate timeProvider.GetUtcNow(); } }测试时var fakeTime new FakeTimeProvider(); fakeTime.SetUtcNow(new DateTimeOffset(2025, 6, 1, 10, 0, 0, TimeSpan.Zero)); var service new CouponService(fakeTime);配置文件也一样。不要写一个AppSettings静态类到处读而是把IConfiguration注入到构造函数。测试时用Dictionary构造一个ConfigurationBuilder想怎么改就怎么改。var config new ConfigurationBuilder() .AddInMemoryCollection(new Dictionarystring, string? { [MaxRetryCount] 3 }) .Build();还有随机数。业务代码里直接new Random()会让测试结果难以预测。正确做法是抽象一个IRandomGenerator接口或者传入一个Random实例测试时用固定种子。这些小依赖看着不起眼但它们往往是测试不稳定的大坑。结合热门话题里的C#上位机场景多说一句上位机项目往往依赖串口、设备SDK、硬件状态最容易写出不可测代码。我建议把设备交互全部封装在适配器接口后面业务逻辑层只依赖接口这样哪怕没有真实硬件也能用测试替身把所有逻辑跑通。这不是测试技巧是架构决策。3.3 反射与私有方法什么情况下真的需要测它C#反射是很多开发者喜欢研究的话题但在TDD里反射的地位很微妙。它的正当用途远没有大家想象的那么多更多时候是设计有问题拿反射来补作业。我见过有人用反射调用私有方法做单元测试理由是这个私有方法逻辑很复杂值得单独测。这种代码我一律建议重构把私有方法提取成内部方法或者直接抽成一个独立的类。测试应该面向公开行为而不是实现细节。私有方法是实现细节测试它等于把测试和实现绑死以后重构一次就得改一次测试。但有一个场景反射是合理工具给老项目补特征测试。老项目有大量不可测代码你没法在不重构的情况下写干净的业务测试。这时可以用反射读取私有字段、调用私有方法先把当前行为锁定下来。比如var field typeof(LegacyService) .GetField(_retryCount, BindingFlags.NonPublic | BindingFlags.Instance); var retryCount (int)field!.GetValue(service);这种代码是明确的技术债能让你在重构前有张安全网。但它只能作为过渡方案长期维护还是要把依赖理顺、把私有逻辑提取出来。搜索引擎里那些C#反射怎么用的帖子帮不了你多少真正干活时你会明白少用反射多用接口。4. 实操案例用TDD实现一个库存扣减服务理论讲多了容易空下面用一个完整案例把TDD流程串起来。这个案例我故意选了一个业务上很常见、但边界条件不少的场景方便演示测试清单、红灯绿灯重构全过程。4.1 项目结构与NuGet包工程结构先搭好。我是按功能建项目的测试项目和主项目分开Inventory/ Inventory.Core/ Inventory.Core.csproj Inventory.Tests/ Inventory.Tests.csproj测试项目需要引用主项目以及以下NuGet包PackageReference IncludeMicrosoft.NET.Test.Sdk Version17.9.0 / PackageReference Includexunit Version2.7.0 / PackageReference Includexunit.runner.visualstudio Version2.5.7 / PackageReference IncludeNSubstitute Version5.1.0 / PackageReference IncludeFluentAssertions Version6.12.0 /领域模型就两个类型StockItem和IInventoryRepository接口。接口定义里包含获取库存和保存库存两个方法。public class StockItem { public string Sku { get; set; } public int Quantity { get; set; } public StockItem(string sku, int quantity) { Sku sku; Quantity quantity; } } public interface IInventoryRepository { TaskStockItem? GetStockAsync(string sku); Task SaveStockAsync(StockItem stock); }4.2 第一个测试库存充足时扣减成功需求第一条库存充足时扣减成功返回true。先写测试public class InventoryServiceTests { private readonly IInventoryRepository _repo; private readonly InventoryService _service; public InventoryServiceTests() { _repo Substitute.ForIInventoryRepository(); _service new InventoryService(_repo); } [Fact] public async Task DeductAsync_WhenStockIsSufficient_ShouldReturnTrue() { _repo.GetStockAsync(SKU001) .Returns(new StockItem(SKU001, 10)); var result await _service.DeductAsync(SKU001, 3); result.Should().BeTrue(); await _repo.Received(1).SaveStockAsync(Arg.IsStockItem(s s.Quantity 7)); } }注意测试里有三个断言返回值是true、保存的库存数量是7、保存方法被调用了一次。其中数量为7是核心业务逻辑这个断言直接验证了扣减计算正确。此时InventoryService还不存在编译失败。先建一个空类让测试失败原因变成断言失败而不是编译失败public class InventoryService { private readonly IInventoryRepository _repo; public InventoryService(IInventoryRepository repo) { _repo repo; } public Taskbool DeductAsync(string sku, int quantity) { throw new NotImplementedException(); } }运行测试红灯如期而至。然后补上最少实现public async Taskbool DeductAsync(string sku, int quantity) { var stock await _repo.GetStockAsync(sku); if (stock null) { return false; } if (stock.Quantity quantity) { return false; } stock.Quantity - quantity; await _repo.SaveStockAsync(stock); return true; }跑测试变绿。但别急着高兴你会发现stock null这个分支是预分支当前测试根本没覆盖到。先留着后面会有专门的测试补上。4.3 第二个测试库存不足时返回false且不扣减需求第二条库存不足时返回false且库存不能变。测试这么写[Fact] public async Task DeductAsync_WhenStockIsInsufficient_ShouldReturnFalse() { _repo.GetStockAsync(SKU001) .Returns(new StockItem(SKU001, 2)); var result await _service.DeductAsync(SKU001, 3); result.Should().BeFalse(); await _repo.DidNotReceive().SaveStockAsync(Arg.AnyStockItem()); }这里有个细节DidNotReceive验证了库存不足时不会调用保存方法这是不扣减在行为上的直接证据。当前实现已经能通过这个测试因为2 3走了return false分支。4.4 第三个测试参数校验与商品不存在补上边界条件。数量为0或负数时抛异常[Theory] [InlineData(0)] [InlineData(-1)] public async Task DeductAsync_WhenQuantityIsInvalid_ShouldThrow(int invalidQuantity) { var act async () await _service.DeductAsync(SKU001, invalidQuantity); await act.Should().ThrowAsyncArgumentOutOfRangeException(); }商品不存在时返回false[Fact] public async Task DeductAsync_WhenSkuNotFound_ShouldReturnFalse() { _repo.GetStockAsync(UNKNOWN).Returns((StockItem?)null); var result await _service.DeductAsync(UNKNOWN, 1); result.Should().BeFalse(); await _repo.DidNotReceive().SaveStockAsync(Arg.AnyStockItem()); }第一个测试会失败因为当前实现没有对数量做校验。补上public async Taskbool DeductAsync(string sku, int quantity) { if (quantity 0) { throw new ArgumentOutOfRangeException(nameof(quantity), 扣减数量必须大于0); } var stock await _repo.GetStockAsync(sku); if (stock null || stock.Quantity quantity) { return false; } stock.Quantity - quantity; await _repo.SaveStockAsync(stock); return true; }第二个测试直接通过因为stock null分支已经处理了。但这里我要坦白stock null || stock.Quantity quantity这个表达式是为了让测试通过而顺手合并的。严格来说null检查和数量不足是两个不同的业务行为应该拆开处理。这个属于重构阶段可以发现的问题我用一个测试同时覆盖两个场景虽然简单但在更复杂的项目里建议把这两个分支拆成两个测试来写失败信息会更清晰。4.5 行为验证确认不是碰巧返回trueTDD有一个容易被忽略的维度行为验证。我们不光要断言返回值还要验证对外部依赖的调用是否符合预期。库存扣减的核心是调用仓储保存了修改后的数据。如果实现里把扣减结果硬编码为true、根本没调用SaveStock上面第一个测试的Received(1)就会抓住这个假实现。这正是TDD保护力的体现——测试的粒度要细到能防止碰巧通过。4.6 并发扣减的讨论与测试边界热门搜索里有一个话题叫C#线程还有一个是查询线程并中止线程。这在库存扣减里很应景如果两个线程同时扣减同一个SKU的库存会发生什么上面的实现天然有超卖风险线程A查到库存10线程B也查到库存10各自扣减3和5最后库存变成了5而不是2——这就是竞态条件。TDD能不能覆盖这种场景单元测试层面很难因为仓储和库存状态都是模拟的没有共享状态。真正要防超卖你不能依赖单元测试而是要回到设计用数据库事务条件更新例如UPDATE Stock SET Quantity Quantity - q WHERE Sku sku AND Quantity q或者引入分布式锁。这些属于集成测试和架构层面的问题TDD能做的是在单元测试里把库存充足才扣减这个业务规则锁死至于并发安全靠数据库和锁来兜底。这给TDD划了一条重要边界单元测试负责逻辑正确性并发一致性、网络故障、硬件异常这些必须靠集成测试和架构设计。别试图用单元测试解决所有问题测试金字塔每一层有每一层的用途。5. 常见问题与故障排查工具和流程都清楚了最后聊聊我实际项目中踩过的坑。这些坑很具体很多是测试代码跑到第3天就坚持不下去的元凶。5.1 测试相互依赖、执行顺序干扰症状很典型单独跑一个测试类全绿跑整个测试套件就随机挂几个。排查方法先看是否有共享状态。比如测试里用了静态的DateTime.Now、共享内存缓存、或者操作了同一个临时文件。我的经验是每个测试必须完全独立。具体做法测试数据用Guid生成唯一值避免主键冲突每个测试自己创建依赖对象不用测试类里的公共字段涉及文件操作时用Path.GetTempFileName()创建独一份的临时文件测试结束删除涉及数据库时要么用事务回滚要么每测试清空数据xUnit默认每个测试类会创建一个实例这已经帮你隔离了大部分状态。真正容易出问题的是静态字段能不用就不用。我见过一个项目在静态字段里缓存配置导致测试一旦并行执行就串数据排查了一整天。5.2 异步测试的坑超时、死锁和错误地同步等待.NET里的异步测试有自己的脾气。最常见的错误是在测试里用.Result或.Wait()等待异步方法[Fact] public void Test_AsyncWithWait_IsBad() { var result _service.DeductAsync(SKU001, 3).Result; // 危险 }这种写法在单元测试环境下还能跑但一旦放到有SynchronizationContext的环境比如某些UI测试宿主、老的ASP.NET环境就容易死锁。标准做法是把测试方法声明为async Task全程用await[Fact] public async Task Test_Async_IsGood() { var result await _service.DeductAsync(SKU001, 3); result.Should().BeTrue(); }如果你在测试里真的是为了验证异步行为还有一个坑是忘记await——测试方法直接返回Task中间那个await没写测试会跳过断言直接通过。解决方案是打开编译器的警告async方法里未等待的Task调用会得到CS4014警告把它当错误处理。对了很多人问我C#怎么查询线程并中止线程我从来不用Thread.Abort因为在.NET Core里它就是颗定时炸弹。正确做法是用CancellationToken协作式取消这不仅更安全而且让代码变成可测试的——你能在测试里主动触发取消断言取消后状态正确。5.3 数据库、文件、外部接口怎么测单元测试里用Mock模拟仓储很容易但集成测试跑真数据库时问题就来了。常见选择有EF Core InMemory、SQLite内存模式、Testcontainers。我的经验是EF Core InMemory和真实SQL Server行为差异很大比如不支持某些SQL函数只能用来测业务逻辑不能替代集成测试。SQLite内存模式更接近真实关系数据库但跟生产环境还是有差距。Testcontainers是终极方案用Docker起一个真实的数据库容器测试结束自动销毁生产环境什么样测试就什么样。有热搜提到Docker拉取镜像时超时这个确实是集成测试的痛点。我的建议是CI环境提前拉好基础镜像缓存避免每次测试都从远程拉。本地开发时如果网络不稳可以先用SQLite顶着CI里再切Testcontainers。测试基础设施也是工程的一部分值得投入维护。对外部HTTP接口的测试我推荐把HttpClient封装在接口后面单元测试时Mock接口而不是直接MockHttpClient。如果你真的需要测HTTP调用细节可以用WireMock之类的方式起一个本地Mock服务器但绝大多数时候接口抽象就够了。顺便提一句如果你用IdentityServer4做认证集成测试时也建议用一个可控的认证服务替身而不是直接打真实的授权服务器。5.4 老项目补测试的切入策略最后说说存量代码。很多人读到这里会想我的项目已经跑了好几年代码一团乱怎么补测试我的建议是由点及面、由死到活。第一步挑核心且稳定的业务方法补特征测试。不用管代码好不好看先记录当前行为这就是安全网。第二步把最影响迭代速度的类挑出来重构每重构一步跑一次特征测试确认行为没变。第三步等关键路径都有测试保护了再逐渐往边缘业务推。覆盖率别强求。老项目补测试前20%最痛苦也最有效抓住核心业务路径就够让团队睡得踏实。别听那些覆盖率必须上80%的口号给老项目设这种目标是逼着团队写垃圾测试。等代码变成可测的了覆盖率自然会上来。写在最后回到开头的追问TDD在C#/.NET生态里到底能不能落地我的答案是能但前提是你得把它当成一种设计方法而不是先写测试再写代码的仪式。我见过太多人写两三天测试就放弃原因不是测试难写而是代码根本难测——依赖藏在方法内部、时间到处用Now、随机数不抽象测试自然进行不下去。先让代码变得可测再谈TDD流程这是这个系列我反复强调的观点。工具选型、依赖注入、行为清单、红灯-绿灯-重构这些方法论在我的项目里真刀真枪跑过也希望它们能在你的项目里跑起来。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻