使用 Moq 测试.NET Core 应用 - Why Moq?
什么是Mock
当对代码进行测试的时候, 我们经常需要用到一些模拟(mock)技术.
绿色的是需要被测试的类, 黄色是它的依赖项, 灰色的无关的类
在一个项目里, 我们经常需要把某一部分程序独立出来以便我们可以对这部分进行测试. 这就要求我们不要考虑项目其余部分的复杂性, 我们只想关注需要被测试的那部分. 这里就需要用到模拟(Mock)技术.
因为, 请仔细看. 我们想要隔离测试的这部分代码对外部有一个或者多个依赖. 所以编写测试代码的时候, 我们需要提供这些依赖. 而针对隔离测试, 并不应该使用生产时用的依赖项, 所以我们使用模拟版本的依赖项, 这些模拟版依赖项只能用于测试时, 它们会使隔离更加容易.
绿色的是需要被测试的类, 黄色是Mock的依赖项
Mock技术带来的优点
使用Mock技术, 可以有如下的优点:
- 提高测试运行速度, 例如可以模拟DB, Web Service等比较慢的服务, 以及算法等.
- 支持并行开发, 例如实际的依赖项还没有完成开发, 或者等待其他团队开发依赖项.
- 提高测试可靠性, 例如有时这个依赖项的bug太多了, 经常由于依赖项的原因导致测试失败, 那么就应该使用mock版本来验证我们自己写的代码.
- 减少开发/测试成本, 有时程序可能依赖一些云服务, 这些服务是按调用次数收费的, 那么就可以使用Mock版本来节省这方面的开资, 当然了最后还是需要使用真正的服务测试才行; 有时候组建依赖项太费劲了, 就用mock版本吧, 省时省力.
- 在有不确定性依赖项的情况下进行测试, 有些依赖项有不确定性, 可能无理由的造成测试失败, 这时候就应该使用mock版本的依赖.
单元测试
Mock技术通常在单元测试中使用, 可以使用xUnit来为.NET Core应用做单元测试, 这里有介绍xUnit的文章: https://www.cnblogs.com/cgzl/p/9178672.html#xunit
那么什么是一个单元?
这个通常是由团队对系统的理解决定, 可以针对一个类, 也可以针对多个类.
单元测试通常具有以下特点:
- 低级别
- 高聚焦
- 执行速度快
- 容易测试所有执行路径上的代码
术语
- Test Double (我认为可以翻译为测试替身), 是所有非真实依赖项的总称.
- Fake, Fake是那种可以正常工作的实现, 尽管可以正常工作, 但是它们不可以用于生产环境, 例如EFCore里的内存数据库提供商.
- Dummy, 有时候, 被测试方法需要一些参数, 但是这些参数实际上并没有用到, 这时就可以创建dummy, 它们的存在只是为了满足调用方法的参数要求.
- Stub, (状态测试). 它可以使用很直接的方式模拟依赖项的行为. 例如我们可以使用Stub把相关数据放到内存里查询而不是查询真实的数据库; 如果某个测试类需要依赖项的某个Property的值, 那么stub就设定这个值就行.
- Mock, (行为/交互测试). 与Stub不同的是, Mock期待的不是返回值, Mock期待的是动作的执行. 它是依赖项的动态包装, 它可以对哪个方法以什么样的顺序被待测试系统(SUT)调用的这个期待行为进行预编程. 也就是说被测试的系统只有按照特定的顺序调用mock依赖项的特定方法, 那么该系统才算测试通过.
还有其它的一些术语就不介绍了, 主要是这四个.
对于Stub 和 Mock ,可以看下面两张图例:
Moq
官网: https://github.com/moq/moq4
Moq框架可以用来创建dummy, stub 和 mock. 在本文里把这三个东西都叫做mock对象吧.
Moq使用一套API来创建stub和mock对象.
准备项目
一个简单的.NET Core控制台项目: https://github.com/solenovex/Moq-Tutorial-Code, 代码是里面的01 before.
该项目非常简单, 是关于球员转会业务, 它目前只有三个类.
TransferApplication, 球员转会申请类:
TransferResult, 转会审批结果枚举:
还有TransferApproval, 转会审批类:
'
当前的逻辑是, 发起球员转会申请后, 进行审批: 如果总费用大于预算, 那么就直接拒绝; 如果总费用不超标, 并且球员小于30岁, 那么就批准; 但如果球员大于30岁, 并且是超级巨星的话, 这将由老板决定.
建立单元测试项目
在解决方案里建立一个xUnit类型的项目:
然后要保证该项目所用到的库都保持最新:
最后别忘了添加对FootballManager项目的引用:
打开Text Explorer, 可以看到里面有一个待测的单元测试:
做一个简单的单元测试
把UnitTest1改成下面这个简单的单元测试:
重新Build后, 可以看到单元测试的名称更新了.
点击Run All, 运行单元测试, 结果成功:
随后再添加一个简单的单元测试:
Build, 后就会出现这个测试:
Run All, 测试也会成功:
添加依赖
这时, 有一些需求的变化, 球员转会审批前, 需要通过体检.
首先在转会申请类里面添加两个球员的属性:
然后添加一个体检的接口:
这两个方法的作用是一样的, 但是调用方法略有不同.
但是此时, 该接口的实现类还没有开发完毕:
在转会审批类里面, 需要添加这个依赖, 使用的是接口:
在单元测试类里面, 我为转会球员添加了这两个属性, 但是审批类会报错, 因为没有加入依赖项:
所以测试的时候需要注入这个依赖项IPhysicalExamination, 但是PhysicalExamination类还没有做完(里面的方法都没有实现), 所以我们无法new出来这个类.
这时, 我们也许可以传null进去?
这时, 项目是不报错了.
跑单元测试, Run All:
测试失败, 抛出NullReferenceException. 而这个异常导致了测试无法正常进行.
所以, 我们需要Moq, 它可以提供一个Mock(模拟)版本的IPhysicalExamination, 并把它传递到审批类的构造函数里.
安装Moq
在单元测试项目添加Moq:
Moq的第一篇先到这.