• 单元测试探析:什么是Stubs、Mocks、Spies、Dummies?带你了解4个核心工具


    单元测试中,对象之间的依赖往往交织到一起,需要拆成各个单元才能逐个击破,这也是单元测试的目的。如何将这些交织到一起的对象拆开,需要一些工具,这些工具业内人们称其为“测试替身”。

    本文作者介绍了单元测试中的4个“测试替身”工具,即Stubs、Mocks,、Spies 和 Dummies。

    Stubs 为被测试对象提供数据,没有任何行为,往往是测试对象依赖关系的上游。

    Spies 被依赖对象的代理,行为往往由被代理的真实对象提供,代理的目的是为了断言程序运行的正确性。

    Mocks 模拟一个具有特性行为的对象,在测试开始前根据期望提供需要的结果。被测试对象往往调用这个对象的方法时,根据条件得到不同的输入,从而满足测试对象的不同场景。例如,mock 数据库的存储层,返回正常数据、空或者丢出异常等情况。

    Dummy 被用来仅仅作为填充参数列表的对象,实际上不会用到它们,对测试结果也没有任何影响。

    以下为作者观点。

    你可能讨厌或喜欢单元测试,这取决于你,但事实是,如果你不理解它们背后的概念,你写测试的效率可能就会弄得一团糟。

    要成为写单元测试的高手,第一个核心步骤是了解其重点。单元测试不是集成测试,它们必须测试单一的代码单元。

    让我们来看看在写单元测试时要用到的4个工具。我指的不是IDE或任何插件或扩展,我指的是概念性的工具:stubs、mocks、spies、 dummies。

    什么是Stubs?

    我经常看到开发人员通过启动一个“测试数据库”来编写与数据库交互的代码的测试,其中测试可以触发“写入”并通过查询数据库进行验证,我认为这是错误的。

    Stubs可以帮助你处理这些情况,即你的代码与第三方服务进行交互。无论是数据库、API还是硬盘上的文件,stubs都提供了使用更简单版本的服务的代码。

    这个Stub会返回一个已知的、可控的值。例如,如果你正在测试一个向数据库写值的函数,你应该编写一个Stub,避免与数据库的交互,但返回一个成功的结果。

    通过这个,你就可以测试当写入操作工作时发生了什么。然后你可以编写另一个Stub(在另一个测试中),返回一个失败的结果,这样你就可以测试你的逻辑中发生处理错误的部分。

    你可以在一个特定的对象中Stub一个函数或一个方法(只要语言允许)。

    因此,让我们快速看一个例子:

    /// the function to test
    function saveUser(usrData, dbConn) {
     
      let q = createQueryFromUser(usrData)
      let result = dbConn.query(q)
     
      return result;
    }
    
    
    //the stub
    makeStub(dbConn, 'query', () => {
      return true;
    })
    
    //the test
    it("should return TRUE when the query succeeds", () => {
      let result = saveUser({
        name: "Fernando",
        password: "1234"
      }, dbConn)
      result.should.be.true
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    上面的例子有几个地方需要解读,同时注意到,虽然这个例子是用伪JavaScript写的,但其概念可以推导到所有语言。

    首先是要测试的函数,现在它是一个接收数据的简单函数,一个数据库连接对象,并依靠一个伪createQueryFromUser函数来创建实际的SQL查询。来自dbConn对象的query方法是与数据库交互的方法,也是我们有兴趣Stub的方法,因为我们不希望query真正启动。

    这里是Stub发挥作用的地方,makeStub函数负责用我们传递的匿名函数(这是一个伪函数,每次只返回TRUE)神奇地覆盖数据库连接的方法query。

    最后,实际的单元测试是利用Stub(因为它之前就被定义了)。这个测试确保我们的函数在进展顺利时返回正确的布尔值(boolean value)。

    上面只是一个例子,告诉你可以从Stubs中受益。说实话,在任何时候,如果你有一个具有动态结果的函数,你就必须找到一种方法来确保每次执行测试时都有相同的结果。所以,Stubs可以帮到你。

    什么是Mocks?

    Mocks就像Stubs的孪生兄弟,它们看起来很像,人们经常把它们混淆,其实它们两个完全不同。

    当Stubs允许你替换或重新定义一个函数或方法时,Mocks允许你在真实的对象/函数上设置预期行为。因此,从技术上讲,你并没有替换对象或函数,你只是告诉它在某些非常特殊的情况下该做什么,除此之外,对象仍然照常工作。

    让我们看一个例子来理解这个定义:想象一下,要测试一个过道补货功能。它从库存中提取物品,并把它们放在正确的过道上。这里测试的关键是,每次我们补充一个过道时,也需要从库存中取出相同数量的元素。

    var inventory = createMock(Inventory("groceries"))
    //set expectations
    inventory.expect("getItems", 10).returns(TRUE).expect("removeFromInventory", 10).returns(TRUE)
    
    var aisle = Aisle("groceries")
    aisle.replenish(10, inventory) //executes the normal flow
    assertion(aisle.isFull(), "equals to", TRUE)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    请记住,在某些情况下,mocks的预期行为会被你所使用的框架自动检查。这就是为什么没有真正的断言来处理期望值的原因,如果它们没有被满足,模拟就会抛出一个异常,测试就不会通过。

    在这个特殊的例子中,预期getItems方法将被调用,其属性为10,它将返回TRUE,它也将调用removeFromInventory函数,其属性也是10。最后返回的结果是TRUE。

    当然,我们可以用Stubs来完成这个任务,但这不是重点,在许多情况下,这些工具可以用于相同或类似的用例。

    Spies到底是什么?

    顾名思义,Spies可以让我们了解被测试代码内部发生了什么,即使我们并没有真正访问到它。我知道,这听起来很诡异,但它有它的用途。

    换句话说,Spies是收集执行信息的Stubs,因此他们最终可以告诉你调用了什么、何时调用了哪些参数。

    想想上面mocks的例子,我们必须事先设定期望值(预期),以确保我们想要的东西都会被执行。我们可以通过 "监视 "库存来检查同样的事情,并询问这些方法是否真的被调用了,用了哪些参数。

    我们来看看另一个例子,一个文件读取器函数,一旦它完成了文件处理,也应该关闭文件处理程序。

    const filename = "yourfile.txt"
    let myspy = new Spy(IOModule, "closeFile") //create a spy for the method closeFile in the module dedicated to I/
    
    function readConfigFile(fname) {
     const reader = new FileReader(filename, IOModule)
     let content = reader.read()
     loadConfig(content)
     IOModule.closeFile(reader);
    }
     
     
    //The test
    
    it("should call the 'closeFile' method after reading the content of the file", () => {
      readConfigFile(filename)
      assertion(myspy.called, "equals to", TRUE)  
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    要测试的函数叫做readConfigFile,它的目的是读取一个文件,并通过调用loadConfig方法将其内容加载为配置。作为测试的一部分,我们有兴趣了解该函数是否真的关闭了文件处理程序。

    请记住,这个测试与我上面所说的相反,因为它实际上是在打开和读取文件,这是我们单元测试不应该有的第三方依赖。为了使这个测试完全 “合规”,当我们有兴趣测试成功的读取和失败的读取时,我们还必须为I0Module和控件添加一个stub。

    注意:与stubs不同的是,Spies包装目标方法/函数,而不是替换它,因此目标的原始代码也将被执行。

    什么是 dummies?

    最后,我想介绍的最后一个工具是众多周知的无用的 “dummies”。顾名思义,除了在需要的时候出现之外,没有其他真正的用途。它们的目的是在语法需要时出现在那里。

    例如,想象一下必须调用一个需要3个参数的函数,其中第一个参数是另一个函数(外部依赖)。考虑到该函数当前的stub,你知道其他两个属性不会被使用,然而,解释器/编译器正在抱怨你缺少该函数的最后两个属性,所以你需要添加它们。

    你怎么能做到这一点呢?

    你猜对了,通过dummies。你只需添加2个什么都不做但被编译器接受的dummy对象。

    Dummies在强类型语言中使用时更有意义,因为这些类型的检查在那里更常见。例如,看看下面这个TypeScript的例子:

    type UserData = {
      name: string;
      password: string
    }
    
    //The function to be tested
    function saveUser(usrData: UserData, dbConn: DataBase, validators:DataValidators) {
     
      if(!validators.validateUserData(usrData)) {
        return false;
      }
      let query = createQueryFromData(usrData);
      let result = dbConn.query(query);
      return result;
    }
    
    // The test itself
    
    //the stub
    const stubbedValidators: DataValidators = {
      validateUserData: (data: UserData) => false;
    }
    
    //the dummies
    const userData: UserData = {name: "", password: ""}
    const dbConn: DataBase = {}
    
    //the test
    it("should return false if the user data is not valid", () => {
      let result = saveUser(userData, dbConn, stubbedValidators);
      result.should.be.false;
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    该代码定义了一个新的saveUser函数,该函数也需要一个validators依赖。我们还添加了一个验证步骤,以确保我们试图保存的数据是 “有效的”(不管这意味着什么)。

    但我们测试的目的是确保如果数据无效,我们将返回false。这意味着我们没有真正执行任何验证,事实上,我们需要stub那个验证器来控制结果,否则如果明天我们的验证例程发生变化,我们现在可能会传递一个有效的数据样本,测试就会失败。

    现在的问题是,通过查看我们的业务逻辑,如果数据是无效的,我们并没有真正使用数据库连接,也没有实际的用户数据。我们需要它们在那里,但我们并不真正需要它们。所以他们实际上已经变成了dummies。

    这就是为什么我只是传递假的空对象(A.K.A dummies)作为函数的前两个属性。

    Stubs, Mocks, Spies和Dummies是你在测试中所做的一切的面包和黄油,你越是使用它们,就越是感觉熟悉,你就越容易理解如何处理一个新的测试。

    例子是否足够清楚?你对这些例子还有疑问吗?请留下评论,我们一起讨论!


    资源分享

    下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】

    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    Centos8 降低gcc版本至gcc-7.3
    Trie字符串统计(c++题解)
    docker实战学习2022版本(六)之Dockerfile整合微服务实战
    TCP重传机制、滑动窗口、流量控制、拥塞控制
    JavaScript使用函数
    ubuntu20 install ros
    蓝牙耳机什么牌子好?口碑最好的蓝牙耳机品牌排行
    摩尔斯电码笔记
    猿创征文|『单片机原理』程序存储器的结构
    kubernetesr进阶--污点和容忍之基于污点的驱逐
  • 原文地址:https://blog.csdn.net/m0_67695717/article/details/127805567