ASP.NET Core 单元测试
前言
单元测试是好, 但是也很花时间. 有些功能封装好了以后也不怎么会再打开, 所以通常就是徒手测试一下, 过了就过了.
但是往往就是那么神奇, 就是会有需求漏掉. 后来要加, 又由于不想潜水, 对自己有信心就会直接改, 然后只测试自己认为影响的部分. 最后就是有 bug.
所以要解决这种问题, 还是写测试比较好,毕竟一个 live 的项目出 bug 有时候会死的很难看.
3 选 1
其实也没什么好选的, NUnit 是 XUnit 的前生, XUnit 是面向 .NET Core 的.
MSTest 是微软自带的, 但是很少人用.
微软官网三个教程都有. 所以自然是选 XUnit 咯.
文档参考
Unit Testing | .NET Core 101 [7 of 8] (YouTube 教程)
dotnet test (.NET CLI)
dotnet test --filter (.NET CLI)
.NET Core Test Explorer (VS Code Plugin)
Testing .NET Core Apps with Visual Studio Code
大致长相
测试通常是开多一个 Project 做. Naming convention 和 Folder structure 我还没有一个规范, 之后再补上.
IDE 都支持的挺好的. 记得要开启 CodeLens 哦.
VS Code
Visual Studio
.NET CLI
定义 Trait 来做 filter
[Fact] [Trait("Category", "Now")]
cmd
dotnet test --filter Category=Now
dotnet test --filter Category!=Now
moq
moq 在 TDD 扮演很重要的角色.
参考: moq4 Quickstart
安装: dotnet add package Moq
Service 通常会有许多依赖, 我们在做单侧时不能直接使用这些依赖, 不然依赖有问题就会被误以为是这个 Service 的问题.
所有依赖的 Service 都需要使用 mock 形式.
比如: Options
var options = new RImageOptions { Ratios = new List<Ratio> { new Ratio { Wide = 16, High = 9 }, new Ratio { Wide = 3, High = 2 }, new Ratio { Wide = 4, High = 3 }, new Ratio { Wide = 1, High = 1 }, new Ratio { Wide = 3, High = 4 }, new Ratio { Wide = 2, High = 3 }, } }; var snapshot = new Mock<IOptionsSnapshot<RImageOptions>>(); snapshot.Setup(s => s.Value).Returns(options); mainService(snapshot.Object);
常见 Error: "may not be used in setup / verification expressions", 参考这里
原理: Mock<Interface> 它会做一个符合这个 interface 的对象, Mock<Class> 它会 inherit 这个 class 并且 override 它的 method,所以这个 class method 必须是 virtual / abstract 才能被 override
override 失败就报错了。所以呢,最好还是用 Interface。
除了上面这种直接依赖, 也有一种情况是, 我们在测试一个方法 A, 它会执行传入的委托/对象.
而我们想确认它是否真的执行了, 或者执行了几次, 执行时传入的参数是否真确等.
要测试的方法
public static void MatchBracket(string value, string bracket, Action<(int start, int end, string valueInBracket)> action) { action((0, 2, "a")); action((4, 6, "b")); action((8, 14, "c {d}")); }
检查委托执行顺序, 参数, 调用次数
[Fact] [Trait("Category", "Now")] public void MatchBracket() { var action = new Mock<Action<(int, int, string)>>(MockBehavior.Strict); var sequence = new MockSequence(); action.InSequence(sequence).Setup(a => a(Tuple.Create(0, 2, "a").ToValueTuple())); // 确保顺序和参数 action.InSequence(sequence).Setup(a => a(Tuple.Create(4, 6, "b").ToValueTuple())); action.InSequence(sequence).Setup(a => a(Tuple.Create(8, 14, "c {d}").ToValueTuple())); Program.MatchBracket("{a} {b} {c {d}}", "{}", action.Object); action.Verify(a => a(It.IsAny<(int, int, string)>()), Times.Exactly(3)); // 确保调用次数 }
更多 pattern 参考 Quickstart 就可以了. 比如 callbacks 也很好用.
Mocking Extension Methods
它没有优雅的实现方法,只有 workaround。不过视乎大家都不介意。
首先,我们有一个 string extension method ToTitleCase,有一个 Person.SayHello 类和方法,这个方法里面调用了 ToTitleCase。
public static class StringExtensions { public static string ToTitleCase(this string stringValue) { return stringValue; } } public class Person { public string SayHello(string name) { return $"Hi, {name.ToTitleCase()}"; } }
现在我们要测试 Person.SayHello,但是又不希望 ToTitleCase 搞鬼,所以需要 mock 这个 ToTitleCase。
测试代码
public class Person_Test { [Fact] public void Do() { var person = new Person(); var result = person.SayHello("derrick"); Assert.Equal("Hi, Derrick", result); } }
由于 ToTitleCase 有 Bug,所以测试失败了。
但其实我们想测试的是 SayHello,SayHello 的实现并没有 Bug,它是被 ToTitleCase 陷害了。
让我们来 Mock ToTitleCase,首先搞一个中间人。
public interface IStringExtensionsImplement { string ToTitleCase(string Value); } public class StringExtensionsImplement : IStringExtensionsImplement { public string ToTitleCase(string stringValue) { return stringValue; } }
StringExtensionsImplement 负责具体的实现。
而 StringExtensions 只是一个简单的 wrapper 而已。
public static class StringExtensions { public static IStringExtensionsImplement Implement { get; set; } = new StringExtensionsImplement(); // 这里让测试可以替换 Mock Implement 进来 public static string ToTitleCase(this string stringValue) { return Implement.ToTitleCase(stringValue); // 具体依靠 Implement 对象 } }
Implement 对象就是中间人。测试的时候我们就可以 Mock 这个对象,然后替换掉 StringExtensions.Implement。
测试代码
public class Person_Test { [Fact] public void Do() { var mockImplement = new Mock<IStringExtensionsImplement>(); mockImplement.Setup(e => e.ToTitleCase(It.IsAny<string>())).Returns("Derrick"); // Mock and setup return var originalImplement = StringExtensions.Implement; StringExtensions.Implement = mockImplement.Object; // 偷龙转风 var person = new Person(); var result = person.SayHello("derrick"); Assert.Equal("Hi, Derrick", result); StringExtensions.Implement = originalImplement; // 替换回去 } }
看懂了吗?其原理就是搞一个中间人来做实现,然后 mock 这个中间人再偷龙转风。