Testify Mock 单元测试

这篇具有很好参考价值的文章主要介绍了Testify Mock 单元测试。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Testify 提供了单测方便的断言能力,这里的断言是将对代码实际返回的断言,代码的实际输出和预期是否一致。下面是 gin-gonic/gin 代码库的单测代码,Testify 还提供了很多其他的方法:

assert.Equal(t, "admin", user)
assert.True(t, found)

单元测试中也会存在不稳定的代码,我们的入参虽然保持不变,但每次单测的结果可能会发生变化。比如说,我们会调用第三方的接口,而第三方的接口可能会发生变化。再比如,代码中有通过 time.Now() 获取最近7天内的用户订单,这个返回结果本身就是随当前时间变化的。

当然,我们肯定不希望每次单测都手动调整,来“迎合”这类不稳定的代码,这样不仅疲于奔命,还效率不高。测试上使用 Mock 就可以解决这类问题。这里主要看看如何使用 Testify 的 mock 功能,从下面这个简单的例子出发:

package main

import (
	"math/rand"
	"time"
)

func DivByRand(numerator int) int {
	rand.Seed(time.Now().Unix())
	return numerator / int(rand.Intn(10))
}

DivByRand 中除以一个随机数,导致结果是随机的,不可预测的,我们该如何对它进行单测呢?特别强调下,rand.Seed 方法调用是必须的,如果不随机初始化 seed,rand.Intn 每次返回的结果都是相同的。随机数都是基于某个 seed 的随机数,seed 不变,预期的随机数就是固定不变的。

我们针对随机方法的部分,做一个接口声明,以及接口实现,来替代代码中随机的被除数。这里mock需要对原函数做代码改造,我们一起来看一下调整过程:

import (
	"github.com/stretchr/testify/mock"
	"testing"
)

// 声明随机接口
type randNumberGenerator interface {
    randomInt(max int) int
}

// 声明接口的实现
type standardRand struct{}

func (s standardRand) randomInt(max int) int {
	rand.Seed(time.Now().Unix())
    return rand.Intn(max)
}

// 修改原有的方法,已经修改了原函数的声明
func DivByRand(numerator int, r randNumberGenerator) int {
	return numerator / r.randomInt(10)
}

使用 Testify mock 功能

我们声明一个 mock 结构体,匿名嵌套 mock.Mock。通过嵌套 Mock,结构体就具备了注册方法,返回预期结果的能力。其中,randomInt 的实现对应了我们预期的输入和输出关系。

type mockRand struct {
	mock.Mock
}

func newMockRand() *mockRand { return &mockRand{} }

func (m *mockRand) randomInt(max int) int {
	args := m.Called(max)
	return args.Int(0)
}

最终,我们的单测就变成了下面的样子,其中的 On 用来对结构体的方法 randomInt 做设置,Return 对应了 args.Int(0)。我们执行下面的单测,返回的结果是恒定的。

func TestDivByRand(t *testing.T) {
	m := newMockRand()
	m.On("randomInt", 10).Return(6)

	t.Log(DivByRand(6, m))
}

Testify Mock 方法实现的核心就是 On 和 Return 方法了,它对应的是我们接口的实现。m.Called 函数的返回值类型为 Arguments,包含了函数的返回值信息,args.Int(0) 表示获取 Called 函数的第一个返回值。On 用来给函数设置入参,Return 用来给函数设置出参。

randomInt 方法只返回了一个结果,一般的函数还会额外返回 error 信息,来标志函数执行是否出现异常。这种情况可以通过 args.Error(1) 来获取,表示第二个出参是 error 类型。

从起初的单测,到最后的单测,函数的方法声明被修改了。假设我们要处理的是线上代码,这样的改动其实破坏了代码的稳定性,为了单测,我们还需要重新对代码做回归测试,确保改动不会对线上环境产生影响。但这种改动也可以总结为一种模式,只要按照固定的模式做代码调整就可以了:

  1. 针对返回结果不确定的方法 ,封装独立的接口声明。
  2. 使用 Testify Mock 重新实现这个接口
  3. 使用 Testify Mock 确定性的实现来替代原来的方法。前提是将之前直接调用方法的地方,修改为接口调用。

所以,这种情况其实不利于存量代码的覆盖率测试,单测最好做到是不要侵入老代码,避免引入不必要风险。但在增量代码上,我们可以使用这种模式,提前按照接口的模式去做功能实现,也就是测试驱动的意思。

在开发代码之前,先想好单测的实现,代码设计上多了一个维度的考量,也会让代码写的更加有扩展性。

库的其他 mock 方法

观察下面的代码,它用到了更多 testify 库提供的方法,包括 MatchedBy、Once、AssertNumberOfCalls。不过就数 AssertNumberOfCalls 最简单了,用来断言方法的调用次数。

mock.MatchedBy(reqSlothFacts) 本来应该是传递函数 ListAnimalFacts 入参的,现在传递了 mock.MatchedBy 函数的执行结果。我们详细来看一下这几个方法。

func TestGetSlothsFavoriteSnackOnPage2(t *testing.T) {
    c := newMockClient()
    c.On("ListAnimalFacts", mock.MatchedBy(reqSlothFacts)).
        Return(&page1, nil).
        Once()
    c.On("ListAnimalFacts", mock.MatchedBy(reqSlothFacts)).
        Return(&page2, nil).
        Once()

    favSnack, err := getSlothsFavoriteSnack(c)
    if err != nil {
        t.Fatalf("got error getting sloths' favorite snack: %v", err)
    }

    if favSnack != "hibiscus flowers" {
        t.Errorf(
            "expected favorite snack to be hibiscus flowers, got %s",
            favSnack,
        )
    }

    c.AssertNumberOfCalls(t, "ListAnimalFacts", 2)
}

Once 和 sync.Once 要表示的含义是一致的,表示只能执行一次,但多次执行 sync.Once 也是没有问题的,只不过只有第一次生效而已。但 mock 中的 Once 执行两次是会报错的。如果只是用来限定方法的执行次数,想一想,也没啥好单测的。沿用上面的代码,我们稍微做些改动

func TestDivByRand(t *testing.T) {
	m := newMockRand()
	m.On("randomInt", 10).Return(6).Once()

	m.randomInt(10)
	m.randomInt(10)
}

执行上面的单测,会发生 panic,提示信息中声明了:assert: mock: The method has been called over 1 times.。就目前来说,难道除了 panic 就没有什么更好的方式了?

其实 Once 还有另一个特别有用的功能,就是设置函数不同的返回值,还拿 randomInt 函数来说,如果我们期望,同样的入参,第一次调用 randomInt 返回6, 第二次调用 randomInt 返回 5 怎么处理。注意,是相同的入参。我们可以通过这样的方式就能输出 6、5。

func TestDivByRand(t *testing.T) {
	m := newMockRand()
	m.On("randomInt", 10).Return(6).Once()
	m.On("randomInt", 10).Return(5).Once()

	t.Log(m.randomInt(10))
	t.Log(m.randomInt(10))
}

当然,如果不使用 Once,用下面这种不同的入参,也能达到相同的效果。改动点主要是 On 方法的第二个参数,以及 Return 方法的返回值。

func TestDivByRand(t *testing.T) {
	m := newMockRand()
	m.On("randomInt", 10).Return(6)
	m.On("randomInt", 9).Return(5)

	t.Log(m.randomInt(10))
	t.Log(m.randomInt(9))
}

最后,我们来看看 MatchedBy 的用法,可以用来校验方法的入参。这个方法的使用约束比较多,参数需要是一个函数,函数的返回只是 bool 类型,校验成功返回 true,校验失败返回 false。函数的入参就是方法的入参类型,且只能处理一个参数,如果方法有多个参数,需要声明多个 MatchedBy。

我们用例子来看一下,我们校验 randomInt 的参数必须等于10,如果不等于10,单测又会抛出 panic。整体来看,MatchedBy 的效用不是特别大。

func TestDivByRand(t *testing.T) {
	m := newMockRand()
	m.On("randomInt", mock.MatchedBy(func(num int) bool {
		return num == 10
	})).Return(6)

	t.Log(m.randomInt(10))
}

本文主要是参照 Mocks in Go tests with Testify Mock 的示例,不过原文章讲的过于详细文章来源地址https://www.toymoban.com/news/detail-438755.html

到了这里,关于Testify Mock 单元测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • mock写单元测试和查数据库的单元测试

    一:mock方式 在测试类上添加注解 将需要测试的类bean添加进来,该类中的其他bean也添加进来 给被测试类中用到的参数、返回值类创建对象 创建BeforeEach和AfterEach方法,在BeforeEach方法中给参数,返回值设置值 然后在test方法中设置被测试的方法 二:可以检测dao层sql的单元测试

    2024年02月15日
    浏览(45)
  • Go 单元测试之mock接口测试

    目录 一、gomock 工具介绍 二、安装 三、使用 3.1 指定三个参数 3.2 使用命令为接口生成 mock 实现 3.3 使用make 命令封装处理mock 四、接口单元测试步骤 三、小黄书Service层单元测试 四、flags 五、打桩(stub) 参数 六、总结 6.1 测试用例定义 6.2 设计测试用例 6.3 执行测试用例代码

    2024年04月22日
    浏览(25)
  • Go语言测试——【单元测试 | Mock测试 | 基准测试】

    作者 :非妃是公主 专栏 :《Golang》 博客主页 :https://blog.csdn.net/myf_666 个性签:顺境不惰,逆境不馁,以心制境,万事可成。——曾国藩 软件测试 :软件测试(英语:Software Testing),描述一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。换句话说,软件测

    2024年02月10日
    浏览(41)
  • 单元测试之Power Mock

    一、简介 EasyMock、Mockito、jMock(单元测试模拟框架) 在有这些模拟框架之前,程序员为了编写某一个函数的单元测试,必须进行十分繁琐的初始化工作,以确保调用的接口或编写的代码得到预期的结果。单元测试模拟框架极大的简化了单元测试的编写过程,在被测试代码需要

    2023年04月08日
    浏览(28)
  • CompletableFuture的单元测试Mock

    在spring项目,假设我们有一个方法 我们对这个方法单元测试,大概率就直接写成: 这样会导致Completable的线程不运行,一直阻塞在红色箭头指示的地方: 等待线程执行完毕。然而线程并没有执行。 此时需要模拟并驱动异步线程的执行,因此需要这样写: 这样就mock了对Runn

    2024年02月09日
    浏览(19)
  • SpringBoot 使用Mock单元测试

    测试一般分为两种黑盒测试和白盒测试。         黑盒测试又称为 功能测试 或 数据驱动测试 ,测试过程中,程序看作成一个黑色盒子,看不到盒子内部代码结构。         白盒测试又称为 结构测试 或 逻辑驱动测试 ,测试过程中,程序看作一个透明盒子,能够看清

    2024年03月20日
    浏览(34)
  • SpringBoot单元测试断言 assertions

    断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别: 1、简单断言 2、数组断言 通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

    2024年02月05日
    浏览(30)
  • java单元测试( Hamcrest 断言)

    单元测试特征: 1 范围狭窄 2 限于单一类或方法 3 体积小 为什么要编写单元测试? 为了防止错误(很明显!) 而且还可以提高开发人员的生产力,因为单元测试: (1) 帮助实施——在编码的同时编写测试可以快速反馈正在编写的代码。 (2) 失败时应该易于理解——每个测试在

    2024年02月06日
    浏览(28)
  • JUnit 5单元测试(二)—— 断言

    上一篇讲了 JUnit 5单元测试(一)—— 基本配置,书接上文开始 JUnit 5单元测试(二)—— 断言 1.单元测试的类名应该起为 xxxxTest.java 表明这个一个测试类,类名应该用简洁的英文表明测试内容或函数。 例如:为了测试一个计算和的方法,可以取名为 SumTest.java 2.测试方法上

    2024年01月16日
    浏览(44)
  • 通过Mock玩转Golang单元测试!

    如果项目中没有单元测试,对于刚刚开始或者说是规模还小的项目来说,效率可能还不错。但是一旦项目变得复杂起来,每次新增功能或对旧功能的改动都要重新手动测试一遍所有场景,费时费力,而且还有可能因为疏忽导致漏掉一些覆盖不到的点。在这个基础上,单元测试

    2024年02月05日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包