测试代码的基本结构以及(Test Double)的种类

2022-01-26  本文已影响0人  李明燮

最近有机会写.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。
每个类型我都写了一些简单的代码,易于理解。

图片备用地址

test_double

Dummy

简单的说,Dummy是一个执行过程中为需要创建冒充的对象,不能保障正常的功能。
看看下面的例子:

public interface People {
    void running();
}

public class PeopleDummy implements People {
    @Override
    public void running() {
        // 不采取任何操作
    }
}

正常是需要实现running()方法,但是在特定的测试场景中不需要这些操作。
这时running()方法不会影响测试内容,也没必要去实现。
像这样不能正常工作也不影响测试,测试时又需要的对象叫做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

简单的说,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是模拟了和生产一样的行为,其行为是简化的。
因为这次项目用的是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

上一篇下一篇

猜你喜欢

热点阅读