蛙蛙推荐:关于单元测试讨论-关于Mock
鸣谢:特别感谢NickWang的悉心指导,我才打消了对Mock是玄学的念头,加深了对单元测试的理解。
需求:
设计一个功能,输入一个整数,把这个整数取平方后加一。
先根据需求设计接口,根据需求,输入参数属于全体整数集,返回值属于正整数集
public interface IMathHelper
{
/// <summary>
/// 实现平方加一的功能
/// </summary>
/// <param name="x">x∈Z</param>
/// <returns>ret∈N+</returns>
int SquareAndIncrease(int x);
}
设计接口的时候要考虑是否有很好的可测试性,然后就可以写单元测试方法
[TestMethod, Description("测试平方加一功能")]
public void SquareAndIncreaseTest() {
IMathHelper mathHelper = null; //这里先空出来
const int arg = 2;
int ret = mathHelper.SquareAndIncrease(arg);
Assert.IsTrue(ret == 5);
}
测试只针对接口的合约进行测试,传入一个arg=2,根据合约2*2+1=5,预期ret=5。
然后设计接口的实现,把该功能设计成两一部分,一部分实现生成平方的功能,一部分实现加一的功能。
假如说实现平方的功能由另一个同事实现,你实现剩余的功能,因为你要调用生成平方的模块(服务),
所以你们之间再设计一个合约,可以用比较轻型的委托来表达:Func<int, int>
然后你就可以实现IMathHelper接口了,其中生成平方的功能是运行时注入的。
public class MathHelperImp : IMathHelper
{
private readonly Func<int, int> _square;
public MathHelperImp(Func<int, int> square)
{
_square = square;
}
public int SquareAndIncrease(int x)
{
return _square(x) + 1;
}
}
你现在要测试你的代码,你的代码的逻辑就是根据一个数,调用另一个模块取平方后再加一,
而你不需要测试取平方的功能,在你测试你的模块的时候,取平方的功能可能还没实现呢。
这时候你可以Mock一个取平方功能的模块来使用,你只要根据这个模块的合约实现简单的输出功能即可,
你不需要关心真正的该模块是如何实现的,比如Func<int, int> sqrMock = x => 4;
它传入任何值都返回4,也就是你的MockObject只针对特定的测试数据2返回符合合约的输出。
这个你就足以测试你的模块了,如下:
[TestMethod, Description("测试平方加一功能")]
public void SquareAndIncreaseTest() {
Func<int, int> sqrMock = x => 4;
IMathHelper mathHelper = new MathHelperImp(sqrMock);
const int arg = 2;
int ret = mathHelper.SquareAndIncrease(arg);
Assert.IsTrue(ret == 5);
}
这就是简单的Mock的使用场景,比较适合这种面向合约的编程,而且是强类型的合约,比如接口,委托。
以前一直感觉Mock是一种玄学,比如你要调用一个从数据库取数据的功能,难道你要Mock一个数据库吗?
如果要对你的服务进行单元测试,需要先通过HTTP从A服务获取配置,再用Remoting去B服务获取凭证,
然后用Socket给你的服务拼包,发包,你的服务还要调用C服务,难道你要Mock一个A服务,B服务,C服务吗?
如果这样的话,写Mock的工作量也许比实现你的服务所花费的时间还多。
针对第一个问题,你要明确你测试什么,你测试的不是数据库的功能,所以你没必要Mock一个完整的数据库。
你在设计的时候就要倾向于做面向合约的设计,把访问数据库的部分设计成一个接口,尽量强类型的接口,
输入什么,输出什么,会抛什么异常都说明清楚,这样合约很清楚的话,根据特定的测试数据能人为计算出输出,
有了这些条件,写一实现合约但只有固定输出功能的对象来作为MockObject就可以了。
如果你设计的接口是比较粗粒度的,比如byte[] send(byte[] arg),输入和输出是按一定的二进制协议拼包,
做这种设计有时候是迫不得已的,所以你在准备测试数据的时候要先根据二进制协议的规则去拼包,然后MockObject
实现的时候也要把固定的输出按二进制协议拼成特定的输出,甚至你的接口如果是在HTTP方式通信的话,你还得
使用一些Http通信的相关类库去写单元测试代码和MockObject的实现代码。
针对这种情况,尽量使用合约设计和使用Mock的思路还是不变的,可以写一些公共的按协议封包,拆包及通信的辅助
类库来帮助快速编写单元测试。
MockObject也不一定就是一个对象,可以是一个只实现某个合约但返回固定输出的一个服务,测试数据是你事先定义
好的,输出是固定的,你测试的时候只使用特定的测试数据就可以完成依赖这个服务的功能的单元测试。
针对第二个问题,如果你真的去把所有获取测试你的服务的前置条件的代码都完成,然后再写你的单元测试代码的话,
这就不是单元测试了,这叫自动化测试,确实需要很大的工作量。所以你要明确你到底在测试什么,你不是在测如何
正确获取测试你的代码的准备工作。
具体来说,你的单元测试所使用的配置数据,可以写死,或者设置成本地可配置的。
如果你要测试的代码,有一些前置条件,需要验证登录的凭证等,你可以在代码里设计一种通用的凭证,写死,只要
发这个凭证,就验证通过(当然,程序真正发布的时候要把这些代码移除),也就是你在设计代码的时候就要考虑
程序的可测试性,你这个模块可能成为其它代码的前置条件时,做一个“后门”为测试使用。
然后你的服务依赖的服务,就用MockObject或者MockService来实现,上面已经说了。
综上,感觉有以下几点:
1、新功能尽量针对合约进行设计,无论是接口,还是协议,评估下可测试性是否良好。
2、如果要分模块,分服务来实现的话,模块或服务间尽量还是设计强类型接口,通信的时候可以使用二进制协议,
HTTP等协议,但上层可以再封装一个强类型层,以便容易实现被调用服务的MockObject。
3、如果一个服务是在整个服务的调用链比较靠前的,可以做一个为测试使用的后门,针对某些固定输入不进行实际
代码逻辑(参数检查,查数据库等),只返回固定的输出。