【测试】依赖注入、表格测试与压力测试

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

几个在Go中进行代码测试的核心技术:单元测试、压力测试与基准测试。它们共同保证了代码的准确性、可靠性与高效性。

单元测试

单元测试又叫做模块测试,它会对程序模块(软件设计的最小单位)进行正确性检验,通常,单元测试是对一个函数封装起来的最小功能进行测试。

在Go中,testing包为我们提供了测试的支持。要点:

  • 需要将测试函数放置到xxx\_test.go文件中
  • 测试函数以TestXxx开头,其中Xxx是测试函数的名称,以大写字母开头
  • 测试函数以 testing.T 类型的指针作为参数,可以使用这一参数在测试中打印日志、报告测试结果,或者跳过指定测试。
func TestXxx(t *testing.T)

简单的加法例子:

// add.go
package add 

func Add(a,b int) int{
    return a+b
}

接下来在add_test.go文件中,书写TestAdd测试函数,并将执行结果与预期进行对比。

// add_test.go
package add
 
import (
    "testing"
)
func TestAdd(t *testing.T) {
    sum := Add(1, 2)
    if sum == 3 {
        t.Log("the result is ok")
    } else {
        t.Fatal("the result is wrong")
    }
}

要执行测试文件,可以执行go test

» go test                                                                                                      jackson@bogon
PASS
ok      github.com/dreamerjackson/xxx/add    0.013s

如果测试结果不符合预期,输出如下。

=== RUN   TestAdd
    add_test.go:13: the result is wrong
--- FAIL: TestAdd (0.00s)
 
FAIL

assert 库对testing.T进行了封装,例如函数assert.Nil 预期传入的参数为nil,而函数assert.NotNil 预期传入的参数不为nil。如果结果不符合预期,则立即报告测试失败。

不过,这样的单元测试其实并不够清晰,特别是当测试的功能逐渐变多的时候,代码还会变得冗余。 那么有没有一种测试方法可以优雅地测试多种功能呢?这就不得不提到表格驱动测试了。

表格驱动测试

表格驱动测试也是单元测试的一种,用一个例子来说明它。下面是我们写的一个字符串分割函数,它的功能类似于strings.Split函数。

// split.go
package split
 
import "strings"
func Split(s, sep string) []string {
    var result []string
    i := strings.Index(s, sep)
    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):]
        i = strings.Index(s, sep)
    }
    return append(result, s)
}

reflect.DeepEqual是Go标准库提供的深度对比函数,它可以对比两个结构是否一致。而如果有多个要测试的用例,reflect.DeepEqual这段对比函数就会重复多次

package split
 
import (
    "reflect"
    "testing"
)
 
//单元测试
func TestSplit(t *testing.T) {
    got := Split("a/b/c", "/")
    want := []string{"a", "b", "c"}
    if !reflect.DeepEqual(want, got) {
        t.Fatalf("expected: %v, got: %v", want, got)
    }
}
 

在表格驱动中,使用Map或者数组来组织用例,我们只需要输入值和期望值,在下面的for循环中就能够复用对比的函数,这就让表格驱动测试在实践中非常受欢迎了。

// split_test.go
package split
 
import (
    "reflect"
    "testing"
)
 
func TestSplit(t *testing.T) {
    tests := map[string]struct {
        input string
        sep   string
        want  []string
    }{
        "simple":       {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
        "wrong sep":    {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
        "no sep":       {input: "abc", sep: "/",   want: []string{"abc"}},
        "trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
    }
 
    for name, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(tc.want, got) {
            t.Fatalf("%s: expected: %v, got: %v", name, tc.want, got)
        }
    }
}

子测试

前面我们看到的例子都是串行调用的,但是在一些场景下,需要通过并发调用来加速测试,这就是子测试为我们做的事情。

使用子测试可以调用testing.TRun函数,子测试会新开一个协程,实现并行。除此之外,子测试还有一个特点,就是会运行所有的测试用例(即使某一个测试用例失败了)。这样在出错时,就可以将多个错误都打印出来。

如下所示,用 t.Run 子测试来测试之前的Split函数,并发测试所有用例。

func TestSplit(t *testing.T) {
    tests := map[string]struct {
        input string
        sep   string
        want  []string
    }{
        "simple":       {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
        "wrong sep":    {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
        "no sep":       {input: "abc", sep: "/", want: []string{"abc"}},
        "trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
    }
 
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) {
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(tc.want, got) {
                t.Fatalf("expected: %#v, got: %#v", tc.want, got)
            }
        })
    }
}

可以看到,当检测到错误时,能够清晰展示出错误用例的信息。

在这里,我们使用了go test -run xxx参数来指定我们要运行的程序。-run后面跟的是要测试的函数名,测试时会模糊匹配该函数名,符合条件的函数都将被测试。

依赖注入

当我们进行单元测试的时候,可能还会遇到一些棘手的依赖问题。

例如一个函数需要从下游的多个服务中获取信息并完成后续的操作。在测试时,如果我们需要启动这些依赖,步骤会非常繁琐,有时候甚至无法在本地实现。

因此,我们可以使用依赖注入的方式对这些依赖进行Mock,这种方式也能够让我们灵活地控制下游返回的数据。

我们以项目中的Flush()为例,在这个例子中,最后的 s.db.Insert 需要我们把数据插入数据库。

func (s *SQLStorage) Flush() error {
    if len(s.dataDocker) == 0 {
        return nil
    }
 
    defer func() {
        s.dataDocker = nil
    }()
    ...
    return s.db.Insert(sqldb.TableData{
        TableName:   s.dataDocker[0].GetTableName(),
        ColumnNames: getFields(s.dataDocker[0]),
        Args:        args,
        DataCount:   len(s.dataDocker),
    })
}
 

但我们其实并不是真的需要一个数据库。让我们新建一个测试文件sqlstorage_test.go,然后实现数据库DBer接口。

// sqlstorage_test.go
type mysqldb struct {
}
 
func (m mysqldb) CreateTable(t sqldb.TableData) error {
    return nil
}
 
func (m mysqldb) Insert(t sqldb.TableData) error {
    return nil
}
 

接着,我们就可以将mysqldb注入到SQLStorage结构中,单元测试如下所示。

func TestSQLStorage_Flush(t *testing.T) {
    type fields struct {
        dataDocker []*spider.DataCell
        options    options
    }
    tests := []struct {
        name    string
        fields  fields
        wantErr bool
    }{
        {name: "empty", wantErr: false},
        {name: "no Rule filed", fields: fields{dataDocker: []*spider.DataCell{
            {Data: map[string]interface{}{"url": "<http://xxx.com>"}},
        }}, wantErr: true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            s := &SQLStorage{
                dataDocker: tt.fields.dataDocker,
                db:         mysqldb{},
                options:    tt.fields.options,
            }
            if err := s.Flush(); (err != nil) != tt.wantErr {
                t.Errorf("Flush() error = %v, wantErr %v", err, tt.wantErr)
            }
            assert.Nil(t, s.dataDocker)
        })
    }
}
 

测试用例中测试了没有Rule字段时的情形,但是程序却直接panic了。这就是单元测试的意义所在,它可以为我们找到一些特殊的输入,确认它们是否仍然符合预期。

经过测试我们发现,由于我们将接口强制转换为了string,当接口类型不匹配时就会直接panic。

ruleName := datacell.Data["Rule"].(string)
taskName := datacell.Data["Task"].(string)
 

要避免这种情况,我们可以对异常情况进行判断:

if ruleName, ok = datacell.Data["Rule"].(string); !ok {
            return errors.New("no rule field")
        }
 
    if taskName, ok = datacell.Data["Task"].(string); !ok {
        return errors.New("no task field")
    }
 

压力测试

有时候,我们还希望对程序进行压力测试,它可以测试随机场景、排除偶然因素、测试函数稳定性等等。

实现压力测试的方法和工具有很多,例如ab、wrk。合理的压力测试通常需要结合实际项目来设计。
我们也可以通过书写Shell脚本来进行压力测试,如下脚本中, 我们可以用go test -c 为测试函数生成二进制文件,并循环调用测试函数。

# pressure.sh
go test -c # -c会生成可执行文件
 
PKG=$(basename $(pwd))  # 获取当前路径的最后一个名字,即为文件夹的名字
echo $PKG
while true ; do
        export GOMAXPROCS=$[ 1 + $[ RANDOM % 128 ]] # 随机的GOMAXPROCS
        ./$PKG.test $@ 2>&1   # $@代表可以加入参数   2>&1代表错误输出到控制台
done
 

以之前的加法函数为例,执行下面的命令即可对测试函数进行压力测试。其中,-test.v 为运行参数,用于输出详细信息。

> /pressure.sh -test.v
 
PASS
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
    add_test.go:17: the result is ok
PASS
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
    add_test.go:17: the result is ok
 

基准测试

Go测试包中内置了Benchmarks基准测试,它可以对比改进后和改进前的函数,查看性能提升效果,也可以供我们探索一些Go的特性。

我们可以用基准测试来对比之前的接口调用与直接函数调用。

package escape
 
import "testing"
 
type Sumifier interface{ Add(a, b int32) int32 }
 
type Sumer struct{ id int32 }
 
func (math Sumer) Add(a, b int32) int32 { return a + b }
 
type SumerPointer struct{ id int32 }
 
func (math *SumerPointer) Add(a, b int32) int32 { return a + b }
 
func BenchmarkDirect(b *testing.B) {
    adder := Sumer{id: 6754}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        adder.Add(10, 12)
    }
}
 
func BenchmarkInterface(b *testing.B) {
    adder := Sumer{id: 6754}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Sumifier(adder).Add(10, 12)
    }
}
 
func BenchmarkInterfacePointer(b *testing.B) {
    adder := &SumerPointer{id: 6754}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Sumifier(adder).Add(10, 12)
    }
}
 

go test 可以加入-gcflags 指定编译器的行为。例如这里的-gcflags “-N -l” 表示禁止编译器的优化与内联,-bench=. 表示执行基准测试,这样我们就可以对比前后几个函数的性能差异了。

» go test -gcflags "-N -l"   -bench=.
BenchmarkDirect-12                      535487740                1.95 ns/op
BenchmarkInterface-12                   76026812                 14.6 ns/op
BenchmarkInterfacePointer-12            517756519                2.37 ns/op
 

BenchMark测试时还可以指定一些其他运行参数,例如-benchmem可以打印每次函数的内存分配情况,-cpuprofile、-memprofile还能收集程序的 CPU 和内存的 profile 文件。

go test ./fibonacci \\
  -bench BenchmarkSuite \\
  -benchmem \\
  -cpuprofile=cpu.out \\
  -memprofile=mem.out
 

这些生成的样本文件我们可以使用pprof工具进行可视化分析。关于pprof工具,我们在之后还会做详细介绍。

总结

介绍了Go中的多种测试技术,包括单元测试、表格驱动测试、子测试、基准测试、压力测试、依赖注入等。灵活地使用这些测试技术可以提前发现系统存在的性能问题。文章来源地址https://www.toymoban.com/news/detail-544583.html

到了这里,关于【测试】依赖注入、表格测试与压力测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • Golang单元测试详解(一):单元测试的基本使用方法

    Golang 中的单元测试是使用标准库 testing 来实现的,编写一个单元测试是很容易的: 创建测试文件:在 Go 项目的源代码目录下创建一个新的文件(和被测代码文件在同一个包),以 _test.go 为后缀名。例如,要测试net包中 dial.go 中的方法,在 net 包中创建一个名字为 dial_test.g

    2024年02月06日
    浏览(49)
  • Golang单元测试举例

    cal.go  cal_test.go 说明:再GoLand中,要运行测试哪个函数可以自行选择 测试文件名必须以_test.go结尾; 测试方法的开头必须是Testxxx()  monster.go  monster_test.go

    2024年02月10日
    浏览(45)
  • Golang 单元测试

    前言 单元测试是通过编写测试函数来完成的,这些函数位于_test.go文件中 步骤 要创建一个单元测试,你需要遵循以下步骤: 在与要测试的代码相同的包中创建一个新的文件,文件名以_test.go结尾 导入 testing 包 编写测试函数,函数名以 Test 开头,接受一个 *testing.T 类型的参数

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

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

    2024年02月05日
    浏览(42)
  • GoLang 单元测试打桩和 mock

    目录 什么是 mock 变量打桩 接口方法/Redis 函数/方法打桩 包函数 成员方法 MySQL sqlmock sqlite mock gorm http mock 源码地址 单测基础        单元测试,顾名思义对某个单元函数进行测试,被测函数本身中用到的变量、函数、资源不应被测试代码依赖,所谓 mock,就是想办法通过 “虚

    2024年02月02日
    浏览(41)
  • ChatGPT生成单元测试实践(Golang)

    目前gpt本质上是续写,所以在待测函数定义清晰的情况下,单元测试可以适当依赖它进行生成。 收益是什么: 辅助生成测试用例测试代码,降低单元测试编写的心智成本 辅助code review,帮助发现代码显式/潜在问题 本文测试环境: gpt: gpt-3.5-turbo go:go 1.17 本文实践场景:企业

    2023年04月20日
    浏览(49)
  • 特性介绍 | MySQL 测试框架 MTR 系列教程(二):进阶篇 - 内存/线程/代码覆盖率/单元/压力测试

    作者:卢文双 资深数据库内核研发 序言: 以前对 MySQL 测试框架 MTR 的使用,主要集中于 SQL 正确性验证。近期由于工作需要,深入了解了 MTR 的方方面面,发现 MTR 的能力不仅限于此,还支持单元测试、压力测试、代码覆盖率测试、内存错误检测、线程竞争与死锁等功能,因

    2024年02月03日
    浏览(50)
  • 单元测试系列 | 如何更好地测试依赖外部接口的方法

    在现在这个微服务时代,我们项目中经常都会遇到很多业务逻辑是依赖其他服务或者第三方接口。工作中各位同学对于这类型场景的测试方式也是五花八门,有些是直接构建一个外部 mock 服务,返回一些固定的 response ;有些是单元测试都不写,直接利用IDE工具,通过 debug 模式

    2024年02月04日
    浏览(35)
  • @Test单元测试注入bean 3种方式

    @ContextConfiguration Spring整合JUnit4测试时,使用注解引入多个配置文件 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {CachingConfig.class}) 如下:     @Resource     private CacheManager cacheManager; 是CachingConfig的bean 在类上添加以下代码: 在执行test之前会先执行启动项目,后对象会

    2024年02月11日
    浏览(40)
  • [已解决]Springboot单元测试时注入bean失败的问题

    SpringBoot扫包的方式,扫描启动类(引导类)所在的包和它的子包 首先看看测试类的包名与启动类的 包名是否一致   因为包名不一致,当启动类启动时就不知道去哪扫描bean,所以也就无法注入。 还有就是注意@SpringBootTest指定的启动类不要导错包 \\\"启动类\\\" 导入自己项目的启动类

    2024年02月11日
    浏览(47)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包