You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

12集成测试一条Happy Path扫天下

你好,我是柳胜。

上一讲,我们学习了单元测试,在验证业务逻辑方面,它的优势在于速度又快,阶段又早。既然单元测试看起来是一个完美的自动化测试方案,那为什么还需要集成测试呢?

我在第二讲的3KU原则说过测试需求首先要找ROI最高的截面来验证。在金字塔模型里ROI最高的就是单元测试如果无法实现才回退到ROI第二高的截面一直到ROI最低的端到端测试。

那集成测试存在的价值,一定是做得了单元测试层面做不到的事,否则,集成测试这个概念就没必要存在。那这些事具体有哪些呢?你要是能找到这些事,就找到了集成测试省力又见效的窍门。今天咱们就一起寻找这个答案。

集成测试和单元测试

上一讲我们学过了代码四象限法则,产品的代码按照业务相关性和依赖程度,可以划分到下面四个象限里。

图片

那集成测试和单元测试分别应该归到第几象限呢?

集成测试,顾名思义,是验证本服务代码和其他进程的服务能不能一起配合工作。在上面的四象限里,集成测试的活动领域就在“依赖代码”象限,而单元测试的活动领域是在“领域代码”象限。

我再用图解的方式划分一下地盘,你会看得更清楚。

图片

这张图里的信息量很大,展示了单元测试和集成测试的各自战场,我来跟你细说一下。

单元测试掌管领域代码的测试这些领域代码只是负责数据计算并不会触及外部依赖。像上一讲的changeEmail方法只是计算出一个新的餐馆数目单元测试只需要验证这个计算逻辑是否正确就好了。

那什么是单元测试测不了的呢? 那就是依赖代码。在FoodCome的代码设计里这些外部的依赖管理交给一个独立的Controller Class去做它负责读写数据库、发送消息等等。这块就是集成测试的领域。

看到这里你脑袋里可能会冒出这样一个问题不对呀单元测试也可以测试外部依赖我们在前面讲过可以Mock外部依赖如果我把Database、MessageBus都Mock了那不就也可以做单元测试了么

你能想到这一层说明你已经关注概念背后真正的事情了。是的如果所有的外部服务都Mock了集成测试就变成了单元测试往另外一个方向如果所有的外部服务都是真实的集成测试又变成了端到端的测试。集成测试就是处在单元测试和端到端测试中间的一个状态。

图片

在这里,我们要关注Mock和Real的优劣势集成测试怎么能做得更聪明一些用最少的工作量获得最大的测试效果。下面我们就展开来说一说。

集成测试测什么?

相比单元测试集成测试有2个特点。

第一集成测试运行速度慢这个时间主要花在2个地方第一个是准备集成测试环境的时间你要先把依赖的外部服务启动起来让环境处在一个健康状态第二个是运行集成测试的时间因为集成测试不像单元测试是进程内工作它是跨进程通讯除了计算时间还要加上网络通讯时间等等。

第二执行集成测试要运行的代码量比单元测试要多。因为它走过的路径更长从网络请求到处理请求再到网络返回结果中间需要经历过n个代码单元还有框架代码库代码等等。

这两个特征告诉我们,集成测试是有比较大的成本的,并且它测试的代码逻辑和单元测试是有重叠的。

本着追求整体最大ROI效益的目标集成测试和单元测试需要协同作战保持一个平衡这个平衡的原则是

1.在单元测试阶段验证尽可能多的业务逻辑,这样能让集成测试关注在外部依赖上。

2.集成测试至少覆盖一条长路径案例叫“Happy Path”。

怎么挑选Happy Path

Happy Path是指一条正常业务的测试案例走尽可能多的外部依赖服务。比如一条案例同时走了Database和MessageBus。

针对上一讲提到的用户修改邮箱功能,我们有几个案例:

1.修改邮箱名从 a@foodcome.com到b@foodcome.com
2.修改邮箱名从 a@example.com到a@foodcome.com
3.修改邮箱名从 a@example.com到b@example.com

哪个案例是Happy Path呢再回头看一下代码

public class UserController
{
   .............
    public void ChangeEmail(int userId, string newEmail)
    {
        .....................
        user.ChangeEmail(newEmail, restaurant);
        _database.SaveUser(user);
        //如果restaurant数量有变化就写数据库发送通知信息
        if(restaurant.numberChanged()){
          _database.SaveRestaurant(restaurant);
          _messageBus.SendEmailChangedMessage(userId, newEmail);
         }
    }
}

我们不难发现案例2符合Happy Path因为它触发了多次与2个外部依赖的交互更新了Databse的用户信息和餐馆信息还触发了消息总线发送一条通知出去。

你可能还想到一个疑问如果我们找不到一个能触发全部外部依赖交互点的Happy Path那怎么办很简单那就再加一条Happy Path。

集成测试用Mock还是Real测试

集成测试领域一个有争议的话题就是外部依赖是用Mock还是用真实的实例。在前面我们讲单元测试是“孤立型”还是“社交型”的时候提到了Mock和Real两种方法都有优劣都有适用的场景可以回看第十讲)。

今天我们详细说说选择Mock还是Real的方法。

首先要看外部依赖的特征我把它划分成2种类型。

1.完全可控依赖
2.不可控依赖

什么是完全可控依赖呢? 这个外部的服务被你的应用独享你也能够控制它的开发和运维那这个服务就是完全可控依赖的。一个典型的例子就是数据库在微服务模式下每一个服务独享一个自己的数据库Schema。

那什么又是不可控依赖与可控依赖相反这个外部的服务不止你的应用调用大家都得遵守一个协议或规范和这个公共的外部服务交互。典型的例子就是外部的支付系统SMTP邮件通知服务等等。

与这两种类型相对应的Mock策略如下

图片

为什么是这样的? 完全可控依赖的服务,虽然是在你的应用之外的一个进程,但你可以把跟它的交互当作是你开发的内部实现。你可以升级数据库版本、修改表格结构、增加数据库函数,只要跟着应用的代码一起修改即可。

这种情况下你可以把这个数据库和你的应用当作一个整体没必要花力气做Mock如果你脑子一抽做了Mock就还要维护Mock的变化恭喜进坑。

而不可控依赖服务就不一样了它是公共的你控制不了它而且你跟它的交互还要遵守一个规范的契约。在这种情况下做Mock就划算了原因有二第一基于契约的Mock的维护成本比较低第二使用Mock可以保证你的应用持续重构向后兼容

分析到这我们就能梳理出FoodCome的Mock策略了。

图片

集成测试的实现

找出了Happy Path也定了Mock策略后就可以动手写代码了。

根据2号案例我们来创建一个测试方法方法名为change_email_from_example_to_foodcome

[Fact]
public void Changing_email_from_example_to_foodcome()
{
    // Arrange
    var db = new Database(ConnectionString);                          
    User user = CreateUser(                                           
        "a@example.com", UserType.customer, db);                                              
    var messageBusMock = new Mock<IMessageBus>();                     
    var sut = new UserController(db, messageBusMock.Object);
    // 调用changeEmail方法
    string result = sut.ChangeEmail(user.UserId, "b@foodcome.com");
    // 校验返回
    Assert.Equal("OK", result);
    // 校验数据库里字段
    object[] userData = db.GetUserById(user.UserId);                 
    User userFromDb = UserFactory.Create(userData);                  
    Assert.Equal("b@foodcome.com", userFromDb.Email);                 
    Assert.Equal(UserType.Restaurant, userFromDb.Type);                
        messageBusMock.Verify(                                           
        x => x.SendEmailChangedMessage(                              
            user.UserId, "b@foodcome.com"),                           
        Times.Once);                                                 
}

上面的代码完成了以下步骤,我特意分点列出来,方便你看清楚每一步。

1.创建真实的数据库连接对象;
2.创建MessageBus的Mock对象
3.把2个依赖注入到被测UserController class里调用changeEmail方法
4.检验数据库里的User状态
5.检验Mock的MessageBus里的消息。

小结

今天我们学习了和外部服务的集成测试的方法在动手之前我们要想明白测什么用什么测Mock还是Real。

测什么怎么测这就是集成测试方案要回答的问题而且这个方案的制定遵循3KU原则也就是尽量不做重复的事把精力和时间花在有价值的地方。

单元测试需要做好业务逻辑的验证集成测试主要是测试与外部依赖的集成集成又有2种策略采用Mock还是Real真实的依赖应该遵循能Real就Real的原则不能Real的再采用Mock如果一股脑Mock所有依赖你会发现集成测试没测到什么有用的逻辑都在Mock上而真正集成时还是会遇到问题。

在集成测试案例的设计上,我提出了Happy Path让你能用最少的工作量做最有效果的事情这对于集成测试刚起步的项目来说十分关键。在Mock策略上也是遵循同样的原则尽量把开发和维护Mock的工作量花在最有价值的外部依赖上。

思考题

在实际工作中你有多个测试案例怎么找出那条Happy Path除了看代码还有别的方法么

欢迎你在留言区跟我交流互动,也推荐你把这讲内容分享给更多同事、朋友。