首先,一个优质的项目,UT是不可或缺的一部分,起先经历的N个项目几乎都是赶功能快步前进式的开发方式,项目质量保证大部分依靠实际使用和手工测试机械点界面来排查,不仅效率低下且很难保证覆盖率等等要求。
这些理论,刚开始我其实是不以为然的,我做过很多项目,没有UT也照样能跑,业务运行也没啥问题,但是后面真正开始写UT了,发现自己代码的很多问题,脸上不屑的笑容渐渐消失……
So,我们需要来看一下ABP vNext官方文档的UT介绍。
Emmm….好吧,没有介绍……
那就只能看代码了,先看一下Abp vNext生成UT的基本项目结构吧。
首先,我们能够看到项目创建后,已经默认针对三层进行了XUnit项目的创建,并且自带一个Samples的UT示例。
先从TestBase开始,这一层貌似包含UT环境的配置以及测试数据生成,我们可以首先在这一层的XXXTestDataSeedContributor类中创建种子数据,将其创建到临时库中,每次启动UT项目测试都会创建本地临时库并生成种子数据。
不过我后面发现一开始种子数据过多会拖慢UT启动速度,这在局部UT测试的时很不友好,因此之后我仅仅把一些必要性的测试数据放到了这里,如果是某些业务UT特定需要的数据,则在业务UT构造的时候再去创建。
这里有一个疑惑的点,种子数据创建后,UT层约定如何使用它?如果使用有问题,会不会造成其他UT的断言出现失败?
比方说,AUT方法删除了一个种子数据,而BUT方法的断言则是判断返回数量为1则为true。
在这个时候,因为A删除了数据,导致B断言失败,我们是否可以认为,UT产生的种子数据仅仅是可读的?亦或者断言逻辑,必须用UT方法产生的临时数据来断言有效性?
过程当中,遇到不少问题, 有些神奇的小技巧,也是翻官方源码翻出来的,下面例举几个吧。
#在UT测试中设置当前用户
在当前UT类声明CurrentUser,覆写AfterAddApplication。
UT方法就可以设置模拟访问用户的属性了。
在UT测试中注入依赖服务
XXXService = GetRequiredService<IXXXService>(); // 这其实没啥好说的,不过可以来充数……
后面的小技巧待补充……
然后,在每个UT层,我们使用常规的XUnit特性[Fact]来标识UT方法,这应该属于UT的基础知识了,大致看了一下,基本上理论层次上熟悉一下UT框架的属性和断言策略,我从网上整理了一下大致如下:
#MSTest、NUnit、xUnit.net 属性对照表
MSTest |
NUnit |
xUnit.net |
Comments |
[TestMethod] |
[Test] |
[Fact] |
标记测试方法. |
[TestClass] |
[TestFixture] |
n/a |
xUnit.net类不需要属性;xUnit.net不需要一个属性。它寻找程序集中所有公共(导出)类中的所有测试方法。 |
[ExpectedException] |
[ExpectedException] |
Assert.Throws Record.Exception |
xUnit.net 取消了ExpectedException属性断言 抛出 ExpectedException . |
[TestInitialize] |
[SetUp] |
Constructor |
我们认为使用[SetUp]通常是不好的。但是,您可以将无参数构造函数实现为直接替换 |
[TestCleanup] |
[TearDown] |
IDisposable.Dispose |
我们认为使用[TearDown]通常是不好的。但是,你可以implementIDisposable.Dispose 作为直接替代。 |
[ClassInitialize] |
[TestFixtureSetUp] |
IUseFixture<T> |
要获得每个per-fixture设置,请在测试类上实现IUseFixture<T> |
[ClassCleanup] |
[TestFixtureTearDown] |
IUseFixture<T> |
要获得每个per-fixture的拆卸,请在测试类上实现IUseFixture<T>。 |
[Ignore] |
[Ignore] |
[Fact(Skip="reason")] |
在[Fact]属性上设置Skip参数以临时跳过测试 |
[Timeout] |
[Timeout] |
[Fact(Timeout=n)] |
在[ Fact ]属性上设置超时参数,以在运行时间过长时导致测试失败。请注意,该xUnit.net以毫秒为单位 |
[TestCategory] |
[Category] |
[Trait] |
|
[TestProperty] |
[Property] |
[Trait] |
在测试中设置任意元数据 |
[DataSource] |
n/a |
[Theory], [XxxData] |
理论(数据驱动测试) |
#MSTest、NUnit、xUnit.net 断言对照表
MSTest |
NUnit |
xUnit.net |
Comments |
AreEqual |
AreEqual |
Equal |
MSTest和xUnit.net支持此方法的泛型版本 |
AreNotEqual |
AreNotEqual |
NotEqual |
MSTest和xUnit.net支持此方法的泛型版本 |
AreNotSame |
AreNotSame |
NotSame |
不相同 |
AreSame |
AreSame |
Same |
相同 |
Contains (on CollectionAssert) |
Contains |
Contains |
包含 |
n/a |
DoAssert |
n/a |
做断言 |
DoesNotContain (on CollectionAssert) |
n/a |
DoesNotContain |
不包含 |
n/a |
n/a |
DoesNotThrow |
确保代码不会引发任何异常 |
Fail |
Fail |
n/a |
xUnit.net 可供替代的: Assert.True(false, "message") |
n/a |
Pass |
n/a |
合格的 |
n/a |
Greater |
n/a |
xUnit.net 可供替代的: Assert.True(x > y) |
n/a |
GreaterOrEqual |
n/a |
更高的质量 |
Inconclusive |
Ignore |
n/a |
不确定的 |
n/a |
n/a |
InRange |
确保某个值在给定的包含范围内(注意:NUnit和MSTest对其arequal方法的InRange支持有限) |
n/a |
IsAssignableFrom |
IsAssignableFrom |
|
n/a |
IsEmpty |
Empty |
空的 |
IsFalse |
IsFalse |
False |
假的 |
IsInstanceOfType |
IsInstanceOfType |
IsType |
为xx类型 |
n/a |
IsNaN |
n/a |
xUnit.net alternative: Assert.True(double.IsNaN(x)) |
n/a |
IsNotAssignableFrom |
n/a |
xUnit.net alternative: Assert.False(obj is Type); |
n/a |
IsNotEmpty |
NotEmpty |
不为空的 |
IsNotInstanceOfType |
IsNotInstanceOfType |
IsNotType |
不为xx类型 |
IsNotNull |
IsNotNull |
NotNull |
不为NUll |
IsNull |
IsNull |
Null |
为null |
IsTrue |
IsTrue |
True |
为真 |
n/a |
Less |
n/a |
xUnit.net alternative: Assert.True(x < y) |
n/a |
LessOrEqual |
n/a |
|
n/a |
n/a |
NotInRange |
确保值不在给定的包含范围内 |
n/a |
Throws |
Throws |
确保代码引发完全相同的异常 |
n/a |
IsAssignableFrom |
n/a |
可从… |
n/a |
IsNotAssignableFrom |
n/a |
不可从… |
功能上使用,大致如上,关于UT设计,具体要看项目对于UT质量的要求了。
简单来讲,我理解的UT必须保证所测试方法的单元性,所谓单元测试,不应该像是集成测试一样依赖其他的方法而导致无法将问题定位到具体的单元上,这让单元测试失去了基本的意义,且UT测试不应该对外界产生依赖性,类似于以下几种,基本上都是UT单元的问题。
- 单个UT方法调用多个验证业务方法。
- UT测试依赖外部数据的正确性。
- ……
UT的断言策略,一般分为有效性和非有效性,你需要判断一个方法正常和异常情况下分别应该是什么样子,据此来设置断言条件这个UT方法才是有意义的。类似于以下几种,基本上都是UT质量的问题。
- 查询的过滤五个条件,但UT测试查询仅验证了三个条件。
- 修改字段有五个字段,但UT断言仅验证了三个字段。
- 未对入参的合法性进行验证,仅使用正常有效数据进行调用。
- ……
上面都是我自己的理解,有可能后面觉得不是这么回事,我还会回来再改……
至于UT命名规范,我找了半天没看到有什么具体的参考案例,现在自己一般都是按照UT对应的测试方法名称来命名了。
比如说CreateOrder的UT就叫CreateOrder,如果一个方法多个断言,可能会对成功和失败的UT方法给予同一个命名加修饰符,叫Success_CreateOrder,Error_CreateOrder,以后如果发现新大陆了再做调整吧。