测试代码的基本结构以及(Test Double)的种类
最近有机会写.net的项目了, 习惯性的研究了如何去写测试代码。
这次查看的资料比以前研究golang测试稍微详细了点。
测试代码的基本结构
目前普遍公认的测试代码模板结构,
分三个部分:arrange, act, assert。分别起的作用如下。
1)arrange: 测试之前需要准备的代码。
2)act: 实际要测试的方法。
3)assert: 结果确认。
看下代码更实在。懒得从新写了。
把以前的golang代码Go项目的测试代码1(基础)粘过来了…^^;;
// add_test.go ==> 文件名 _test.go 结尾的默认为测试代码文件
package models
import (
"testing"
)
//Test开头的默认为测试方法
func TestAdd(t *testing.T) {
//arrange
var x, y, res int
x = 2
y = 3
//act
result = Add(x, y)
//assert
if result != 5 {
t.Fatal("Add的结果不正确")
}
}
这是我们能看到的最简单的测试代码, 但是现实和理想还是有差距的。
当我们进行测试的时候, 很多情况是跟其他模块有依赖或调用的关系的。
为了解决这些问题我们会使用测试替身(Test Double)。
测试替身(Test Double)
测试替身起源于拍电影时,拍摄一些高难度或是危险动作时会用替身演员。
Test Double(测试替身) 也是当我们需要用一些功能和依赖项时,可以使用“替身”。
这样我们可以专注于我们需要测试的代码,而不需要把时间浪费在"周围的关系"中。
测试替身的类型可以分为 Dummary、Fake、Stub、Spy、Mock。
每个类型我都写了一些简单的代码,易于理解。
Dummy
- 最简单、最原始的测试替身。
- 需要实体类,但是不需要其功能的时候会用。
- 调用Dummy类的函数不保障正常的使用。
- 只传对象,但不使用,通常只是被用来填充参数列表。
简单的说,Dummy是一个执行过程中为需要创建冒充的对象,不能保障正常的功能。
看看下面的例子:
public interface People {
void running();
}
public class PeopleDummy implements People {
@Override
public void running() {
// 不采取任何操作
}
}
正常是需要实现running()方法,但是在特定的测试场景中不需要这些操作。
这时running()方法不会影响测试内容,也没必要去实现。
像这样不能正常工作也不影响测试,测试时又需要的对象叫做Dummy对象。
Stub
- Dummy的上一级,可以模拟可执行的Dummy对象。
- Stub 是接口或基类的最低限度的实现。
- 针对测试时的请求只返回约定好的结果值。
- 它是对状态的验证。
简单的说,Stub起到的作用是只返回约定好的结果值。
看看下面的例子:
public class StubUser implements User {
// ...
@Override
public User findById(long id) {
return new User(id, "Test User");
}
}
看上述代码调用StubUser类的findById方法时,返回接受的id和name是Test User的实体对象。
像这样只为测试返回特定值得对象叫做stub。
Spy
- Stub的上一级,比起stub多记录自身被调用的情况。
- 测试时记录自身对象是否被调用,举例子的话邮件服务会记录发送了多少封邮件。
- 还会记录发了哪些成员,好让单元测试验证所调用的成员是否符合预期。
简单的说,Spy是记录特定函数是否正常的调用的记录。
顺便也可以当做Stub使用。
看看下面的例子:
public class MailService {
private int sendMailCount = 0;
private Collection<Mail> mails = new ArrayList<>();
public void sendMail(Mail mail) {
sendMailCount++;
mails.add(mail);
}
public long getSendMailCount() {
return sendMailCount;
}
}
这里调用MailService的sendMail方法时,会记录发送的次数。
后续可以通过getSendMailCount()获取发邮件的次数。
像这样存储调用记录的类叫做Spy。
Fake
- 具有可以正常工作的实现,不是完整的生产对象,是把复杂的内容简化后的对象。
- 通常采用了一些不适合生产环境的便捷手段。(一个典型例子是内存数据库)。
简单的说Fake是模拟了和生产一样的对象,但是其内容是简化的。
看看下面的例子:
public class User {
private Long id;
private String name;
protected User() {}
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
}
public interface IUserService {
void save(User user);
User findById(long id);
}
public class FakeUserService implements IUserService {
private Collection<User> users = new ArrayList<>();
@Override
public void save(User user) {
if (findById(user.getId()) == null) {
user.add(user);
}
}
@Override
public User findById(long id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}
}
上面是一个很简单的使用Fack对象的例子。
用实际UserService里的保存和查询方法需要连接数据库。
在测试环境下,不影响主要测试内容的前提下,
可以使用Fack类来取代实际连数据库的操作。
像这样使用摸你的虚假类叫Fack。
Mock
- Mock是虚假的行为,根据预先编写的逻辑,返回期望值。
- 如果接收到没有预先编写的请求,可以抛出异常或空值。
简单的说Mock是模拟了和生产一样的行为,其行为是简化的。
因为这次项目用的是C#,Mock方式是基于比较熟悉的Moq写的,只看看大致的方式就行。
看看下面的例子:
public interface IUserService
{
bool IsHealthy(int weight, int height);
}
public class UserService : IUserService
{
public bool IsHealthy(int weight, int height)
{
/*假想一下这里有很多逻辑和其他模块的依赖...^^;;*/
if (weight == 0 || height == 0)
{
return false;
}
if (weight / (height * height) >= 18.5 && weight / (height * height) <= 23.9)
{
return true;
}
return false;
}
}
public class UserServiceTest
{
[Fact]
public void MockTest()
{
//这里创建了一个Mock的service, IsHealthy()函数可以想象成依赖了其他模块。
//给它指定了一个模拟的行为:接受100,170或是 110,170就返回true;其他都返回false了。(没有走实际的逻辑)
Mock<IUserService> userServiceMock = new Mock<IUserService>();
userServiceMock.Setup(x => x.IsHealthy(100, 170)).Returns(true);
userServiceMock.Setup(x => x.IsHealthy(110, 170)).Returns(true);
IUserService userService = userServiceMock.Object;
Assert.True(userService.IsHealthy(100, 170));
Assert.True(userService.IsHealthy(110, 170));
Assert.False(userService.IsHealthy(70, 180));
}
}
如上述代码,做一些用户操作时需要先验证是否健康。
但是这验证健康的服务可能是第三方服务。
这时候我们会创建一个Mock行为。给指定参数的时候直接返回结果,模拟健康验证行为。
专注于其他业务的测试。
那Fack和Mock有什么区别啊? 其实就是概念上的区别。
Fack是虚假类,Mock是虚假行为。
实战中偏向于虚假行为的是Mock。
偏向于虚假类的是Fack。
实际开发中你会发现很多情况下,一个虚假的类会包含着虚假的行为。
总结
Dummy是一个空壳。
Stub是给这个Dummy空壳加了些线路让他能模拟运行。
Spy是给这个Stub加了存储器,让他有些记忆。
到了Fake和Mock就是比较完整的替身了。
那Fake和Mock作用是什么?
Fack是一种实体的模拟(虚假实例),而Mock是对行为逻辑的模拟(虚假行为)。
概念归概念,实际开发中还是需要实事求是,
适用最合适与自己项目的方式就行,没必要过分的分清他们之间的界限。
欢迎大家的意见和交流
email: li_mingxie@163.com