第 22 章 - Go语言 测试与基准测试

在Go语言中,测试是一个非常重要的部分,它帮助开发者确保代码的正确性、性能以及可维护性。Go语言提供了一套标准的测试工具,这些工具可以帮助开发者编写单元测试、表达式测试(通常也是指单元测试中的断言)、基准测试等。

单元测试 (Unit Testing)

单元测试主要用于验证程序中最小可测试单元的正确性,如一个函数或方法。Go语言使用testing包来支持单元测试。

示例

假设我们有一个简单的加法函数Add,我们想要为这个函数编写单元测试。

源代码: add.go

package main

import "fmt"

// Add two integers and return the result.
func Add(a int, b int) int {
    return a + b
}

func main() {
    fmt.Println(Add(1, 2))
}

测试代码: add_test.go

package main

import (
    "testing"
)

// TestAdd checks if the Add function works as expected.
func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
        {5, -3, 2},
    }

    for _, tt := range tests {
        testname := fmt.Sprintf("%d+%d", tt.a, tt.b)
        t.Run(testname, func(t *testing.T) {
            ans := Add(tt.a, tt.b)
            if ans != tt.want {
                t.Errorf("got %d, want %d", ans, tt.want)
            }
        })
    }
}

在这个例子中,我们定义了一个测试表tests,其中包含了一些输入值和预期结果。对于每个测试用例,我们调用Add函数并检查返回的结果是否符合预期。如果不符合,我们使用t.Errorf报告错误。

表达式测试 (Expression Testing)

表达式测试通常是指在单元测试中使用断言来简化测试逻辑。虽然Go语言的标准库没有直接提供断言功能,但是可以通过第三方库如testify/assert来实现更简洁的测试代码。

使用 testify 的测试代码: add_test.go

package main

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

func TestAddWithAssert(t *testing.T) {
    assert.Equal(t, 3, Add(1, 2), "1+2 should equal 3")
    assert.Equal(t, 0, Add(0, 0), "0+0 should equal 0")
    assert.Equal(t, 0, Add(-1, 1), "-1+1 should equal 0")
    assert.Equal(t, 2, Add(5, -3), "5-3 should equal 2")
}

这里使用了testify/assert库的assert.Equal函数来比较期望值和实际值,并且提供了失败时的错误信息。

基准测试 (Benchmark Testing)

基准测试用于测量代码的性能。Go语言的testing包也支持基准测试。

基准测试代码: add_benchmark_test.go

package main

import "testing"

// BenchmarkAdd measures the performance of the Add function.
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

在这个基准测试中,我们通过循环调用Add函数来测量其执行速度。b.N是测试框架自动设置的一个变量,表示应该执行的次数。运行基准测试可以使用命令go test -bench=.

以上就是Go语言中单元测试、表达式测试和基准测试的基本介绍和示例。希望这些内容能帮助您更好地理解和使用Go语言的测试功能。

接下来,我们将进一步深入探讨一些高级特性和最佳实践,包括子测试、表格驱动测试、并发测试、测试覆盖率以及如何组织测试代码。

子测试 (Subtests)

子测试允许在一个测试函数内部组织多个相关的测试用例。这有助于提高测试的可读性和组织性,特别是在处理复杂的测试场景时。

改进后的测试代码: add_test.go

package main

import (
    "testing"
)

func TestAddSubtests(t *testing.T) {
    cases := []struct {
        name     string
        a, b, want int
    }{
        {"positive", 1, 2, 3},
        {"zero", 0, 0, 0},
        {"negative", -1, 1, 0},
        {"mixed", 5, -3, 2},
    }

    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            got := Add(c.a, c.b)
            if got != c.want {
                t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want)
            }
        })
    }
}

在这个例子中,我们使用t.Run来创建子测试,每个子测试都有一个名称,这样在测试失败时可以更容易地定位到具体的问题。

并发测试 (Concurrent Testing)

Go语言的并发模型非常适合编写高效的测试代码。使用testing.TParallel方法可以在多个CPU核心上并行运行测试,从而加快测试速度。

并发测试代码: add_parallel_test.go

package main

import (
    "testing"
)

func TestAddParallel(t *testing.T) {
    cases := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
        {5, -3, 2},
    }

    for _, c := range cases {
        c := c // 必须创建一个新的副本
        t.Run(fmt.Sprintf("%d+%d", c.a, c.b), func(t *testing.T) {
            t.Parallel()
            got := Add(c.a, c.b)
            if got != c.want {
                t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want)
            }
        })
    }
}

在这个例子中,我们使用t.Parallel()来标记每个子测试可以并行运行。需要注意的是,每个子测试必须是线程安全的,因此我们在循环中创建了新的副本c

测试覆盖率 (Test Coverage)

测试覆盖率是一种度量测试代码质量的方法,它显示了哪些代码路径已经被测试覆盖。Go语言提供了内置的工具来生成测试覆盖率报告。

生成测试覆盖率报告:

go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

第一行命令会生成一个覆盖率文件coverage.out,第二行命令会将这个文件转换为HTML格式,以便于查看。

组织测试代码 (Organizing Test Code)

良好的测试代码组织结构可以使项目更加清晰和易于维护。通常,测试文件应与被测试的源文件放在同一个目录下,并以_test.go结尾。

目录结构示例:

myproject/
├── add.go
├── add_test.go
└── main.go

每个源文件都应该有一个对应的测试文件,这样可以确保每个功能模块都有相应的测试。

总结

通过上述内容,我们可以看到Go语言的测试工具非常强大和灵活,能够满足各种测试需求。无论是简单的单元测试、复杂的并发测试还是详细的基准测试,Go语言都能提供有效的支持。希望这些内容对您有所帮助,如果您有任何问题或需要进一步的指导,请随时告诉我!

接下来我们将进一步探讨一些更高级的测试技巧和最佳实践,包括模拟(Mocking)、依赖注入、测试辅助函数、以及如何处理外部依赖。

模拟 (Mocking)

在测试中,模拟(Mocking)是一种常用的技术,用于隔离被测试代码与其他组件的依赖关系。通过模拟,我们可以控制外部依赖的行为,从而更好地测试我们的代码。

使用 gomock 进行模拟

gomock 是一个流行的 Go 语言模拟库,可以帮助我们生成和管理模拟对象。

安装 gomockmockgen:

go get github.com/golang/mock/mockgen

定义接口:

假设我们有一个 Calculator 接口,我们需要测试一个使用该接口的函数。

calculator.go:

package main

type Calculator interface {
    Add(a, b int) int
    Subtract(a, b int) int
}

生成模拟对象:

使用 mockgen 生成模拟对象。

mockgen -source=calculator.go -package=main > calculator_mock.go

生成的模拟对象:

calculator_mock.go:

package main

import (
    "reflect"
    "testing"
)

// MockCalculator is a mock of Calculator interface
type MockCalculator struct {
    mock.Mock
}

// Add provides a mock function with given fields: a, b
func (_m *MockCalculator) Add(a int, b int) int {
    ret := _m.Called(a, b)
    var r0 int
    if rf, ok := ret.Get(0).(func(int, int) int); ok {
        r0 = rf(a, b)
    } else {
        r0 = ret.Get(0).(int)
    }
    return r0
}

// Subtract provides a mock function with given fields: a, b
func (_m *MockCalculator) Subtract(a int, b int) int {
    ret := _m.Called(a, b)
    var r0 int
    if rf, ok := ret.Get(0).(func(int, int) int); ok {
        r0 = rf(a, b)
    } else {
        r0 = ret.Get(0).(int)
    }
    return r0
}

测试代码:

calculator_test.go:

package main

import (
    "testing"
    "github.com/golang/mock/gomock"
)

func TestUseCalculator(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockCalc := NewMockCalculator(ctrl)
    mockCalc.EXPECT().Add(1, 2).Return(3)
    mockCalc.EXPECT().Subtract(5, 3).Return(2)

    result := UseCalculator(mockCalc)
    if result != 5 {
        t.Errorf("UseCalculator() = %d; want 5", result)
    }
}

func UseCalculator(calc Calculator) int {
    sum := calc.Add(1, 2)
    diff := calc.Subtract(5, 3)
    return sum + diff
}

在这个例子中,我们使用 gomock 生成了 Calculator 接口的模拟对象,并在测试中设置了期望的行为。然后,我们调用 UseCalculator 函数并验证其返回值。

依赖注入 (Dependency Injection)

依赖注入是一种设计模式,通过它可以在运行时将依赖项传递给对象,而不是在对象内部硬编码这些依赖项。这使得代码更灵活、更易于测试。

示例:

假设我们有一个 Service 类,它依赖于 Calculator 接口。

service.go:

package main

type Service struct {
    calc Calculator
}

func NewService(calc Calculator) *Service {
    return &Service{calc: calc}
}

func (s *Service) Compute(a, b int) int {
    sum := s.calc.Add(a, b)
    diff := s.calc.Subtract(a, b)
    return sum + diff
}

测试代码:

service_test.go:

package main

import (
    "testing"
    "github.com/golang/mock/gomock"
)

func TestServiceCompute(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockCalc := NewMockCalculator(ctrl)
    mockCalc.EXPECT().Add(1, 2).Return(3)
    mockCalc.EXPECT().Subtract(1, 2).Return(-1)

    service := NewService(mockCalc)
    result := service.Compute(1, 2)
    if result != 2 {
        t.Errorf("Service.Compute() = %d; want 2", result)
    }
}

在这个例子中,我们通过构造函数将 Calculator 接口的实现传递给 Service 对象,从而实现了依赖注入。

测试辅助函数 (Helper Functions)

测试辅助函数可以帮助减少重复代码,使测试代码更加简洁和可维护。

示例:

假设我们有一个通用的辅助函数来验证两个整数是否相等。

test_helpers.go:

package main

import (
    "testing"
)

func AssertEqual(t *testing.T, got, want int, msg string) {
    if got != want {
        t.Errorf("%s: got %d, want %d", msg, got, want)
    }
}

使用辅助函数的测试代码:

add_test.go:

package main

import (
    "testing"
)

func TestAddWithHelper(t *testing.T) {
    cases := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
        {5, -3, 2},
    }

    for _, c := range cases {
        got := Add(c.a, c.b)
        AssertEqual(t, got, c.want, fmt.Sprintf("Add(%d, %d)", c.a, c.b))
    }
}

在这个例子中,我们定义了一个 AssertEqual 辅助函数,并在测试代码中使用它来简化断言逻辑。

处理外部依赖 (Handling External Dependencies)

在测试中处理外部依赖(如数据库、网络服务等)是一个常见的挑战。通常的做法是使用内存数据库、模拟服务器或配置文件来模拟外部依赖。

示例:

假设我们有一个函数需要访问数据库。

database.go:

package main

import (
    "database/sql"
    "fmt"
)

type DB interface {
    QueryRow(query string, args ...interface{}) *sql.Row
}

func FetchUser(db DB, id int) (string, error) {
    var name string
    err := db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
    if err != nil {
        return "", err
    }
    return name, nil
}

测试代码:

database_test.go:

package main

import (
    "testing"
    "github.com/DATA-DOG/go-sqlmock"
)

func TestFetchUser(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
    }
    defer db.Close()

    rows := sqlmock.NewRows([]string{"name"}).AddRow("Alice")
    mock.ExpectQuery("SELECT name FROM users WHERE id = ?").WithArgs(1).WillReturnRows(rows)

    name, err := FetchUser(db, 1)
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    if name != "Alice" {
        t.Errorf("expected Alice, got %s", name)
    }

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }
}

在这个例子中,我们使用 go-sqlmock 库来模拟数据库连接和查询,从而避免了对实际数据库的依赖。

总结

通过上述内容,我们可以看到 Go 语言提供了丰富的工具和库来支持各种测试需求。从简单的单元测试到复杂的并发测试和外部依赖处理,Go 语言都提供了强大的支持。希望这些内容对您有所帮助,如果您有任何问题或需要进一步的指导,请随时告诉我!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921114.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

冲破AI 浪潮冲击下的 迷茫与焦虑

在这个科技日新月异的时代&#xff0c;人工智能如汹涌浪潮般席卷而来&#xff0c;不断改变我们的生活。你是否对 AI 充满好奇&#xff0c;却不知它将如何改变你的工作与生活&#xff1f;又是否会在 AI 浪潮的冲击下陷入迷茫与焦虑&#xff1f;《AI 时代&#xff1a;弯道超车新思…

【FRP 内网穿透 从0到1 那些注意事项】

【摘要】 最近跟第三方团队调试问题&#xff0c;遇到一个比较烦的操作。就是&#xff0c;你必须要发个版到公网环境&#xff0c;他们才能链接到你的接口地址&#xff0c;才能进行调试。按理说&#xff0c;也没啥&#xff0c;就是费点时间。但是&#xff0c;在调试的时候&#…

基于SpringBoot+RabbitMQ完成应⽤通信

前言&#xff1a; 经过上面俩章学习&#xff0c;我们已经知道Rabbit的使用方式RabbitMQ 七种工作模式介绍_rabbitmq 工作模式-CSDN博客 RabbitMQ的工作队列在Spring Boot中实现&#xff08;详解常⽤的⼯作模式&#xff09;-CSDN博客作为⼀个消息队列,RabbitMQ也可以⽤作应⽤程…

进度条程序

目录 1.回车与换行 2.缓冲区 强制刷新&#xff1a;fflush 策略 3.倒计时程序 4.进度条 4.1先做一下基本的准备工作 4.2现在我们正式来实现 进度: 比率: 旋转光标 表明动态变化: 4.3如果我们要完成一个下载任务 4.3.1实现: 4.3.2光标显示: 4.3.2.1证明一下&#…

软件测试——自动化测试常见函数

在上一篇文章软件测试——自动化测试概念篇-CSDN博客中&#xff0c;给大家演示了一下自动化程序&#xff0c;而本篇文章会带大家详细学习selenium库。 selenium库是python官方的库&#xff0c;里面包含了很多操控浏览器的函数。 本节重点 元素定位操作测试对象窗口等待导航弹…

STM32F103C8T6实时时钟RTC

目录 前言 一、RTC基本硬件结构 二、Unix时间戳 2.1 unix时间戳定义 2.2 时间戳与日历日期时间的转换 2.3 指针函数使用注意事项 ​三、RTC和BKP硬件结构 四、驱动代码解析 前言 STM32F103C8T6外部低速时钟LSE&#xff08;一般为32.768KHz&#xff09;用的引脚是PC14和PC…

AI社媒引流工具:解锁智能化营销的新未来

在数字化浪潮的推动下&#xff0c;社交媒体成为品牌营销的主战场。然而&#xff0c;面对海量的用户数据和日益复杂的运营需求&#xff0c;传统营销方法显得力不从心。AI社媒引流王应运而生&#xff0c;帮助企业在多平台中精准触达目标用户&#xff0c;提升营销效率和效果。 1.…

知识中台:提升企业知识管理的智能化水平

在数字化转型的浪潮中&#xff0c;企业知识管理的智能化水平成为提升竞争力的关键。HelpLook知识中台通过集成先进的AI技术&#xff0c;为企业提供了一个智能化的知识管理平台。 一、知识管理智能化的重要性 智能化的知识管理不仅能够提高信息检索的效率&#xff0c;还能通过…

Unreal5从入门到精通之EnhancedInput增强输入系统详解

前言 从Unreal5开始,老版的输入系统,正式替换为EnhancedInput增强型输入系统,他们之间有什么区别呢? 如果有使用过Unity的同学,大概也知道,Unity也在2020版本之后逐渐把输入系统也升级成了新版输入系统,为什么Unreal和Unity都热衷于升级输入系统呢?这之间又有什么联系…

C语言数据结构与算法--简单实现队列的入队和出队

&#xff08;一&#xff09;队列的基本概念 和栈相反&#xff0c;队列(Queue)是一种先进先出&#xff08;First In First Out&#xff09;的线性表。只 允许在表的一端进行插入&#xff0c;而在另一端删除元素&#xff0c;如日常生活中的排队现象。队列中 允许插入的一端叫队尾…

docker搭建私有仓库,实现镜像的推送和拉取

1.拉取docker仓库镜像 docker pull registry 2.启动registry容器 docker run -d registry 3.查看当前仓库中存在的镜像&#xff08;一&#xff09; curl -XGET http://192.168.111.162: 5000/v2/_catalog 192.168.111.162 部署docker仓库宿主机的ip 5000 部署docker仓库映射到宿…

算法学习笔记(九):网格图DFS、图论算法DFS、动态规划DP、贪心

一.网格图DFS 适用于需要计算连通块个数、大小的题目 1.岛屿数量 给你一个由 1(陆地) 和 0&#xff08;水&#xff09;组成的二维网格&#xff0c;请你计算网格中岛屿的数量 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和\或竖直方向上相邻的陆地连接形成 此外&…

Cmakelist.txt之Linux-redis配置

1.cmakelist.txt cmake_minimum_required(VERSION 3.16) ​ project(redis_linux_test LANGUAGES C) ​ ​ ​ add_executable(redis_linux_test main.c) ​ # 设置hiredis库的头文件路径和库文件路径 set(Hiredis_INCLUDE_DIR /usr/local/include/hiredis) set(Hiredis_LIBRA…

【Node.js】Node.js 和浏览器之间的差异

Node.js 是一个强大的运行时环境&#xff0c;它在现代 JavaScript 开发中扮演着重要角色。然而&#xff0c;许多开发者在使用 Node.js 时常常会感到困惑&#xff0c;尤其是与浏览器环境的对比。本文将深入探讨 Node.js 和浏览器之间的差异&#xff0c;帮助你全面理解两者的设计…

【物联网原理与应用】实验二:红外传感实验

目录 一、实验目的 二、实验原理 三、实验内容及步骤 四、实验结果 五、核心代码 一、实验目的 学习试验模块上线路的连接操作理解掌握红外传感器的工作原理实现对红外传感器数据的接收和处理 二、实验原理 1、将红外辐射能转换成电能的光敏元件称为红外传感器&#…

PAL(Program-Aided Language Model)

PAL&#xff08;Program-Aided Language Model&#xff09;是一种结合生成式语言模型&#xff08;如 GPT&#xff09;和程序执行能力的技术框架。它的核心思想是通过让语言模型生成代码或程序来解决复杂任务&#xff0c;程序执行的结果反过来增强语言模型的输出准确性和逻辑性。…

java基础概念36:正则表达式1

一、正则表达式的作用 作用一&#xff1a;校验字符串是否满足规则&#xff1b;作用二&#xff1a;在一段文本中查找满足要求的内容。——爬虫 二、正则表达式 2-1、字符类 示例&#xff1a; public static void main(String[] args) {System.out.println("a".matc…

VsCode 插件推荐(个人常用)

VsCode 插件推荐&#xff08;个人常用&#xff09;

工业储能柜的大小该如何选择,工商储能系统设备哪家好?

在能源转型和可持续发展的大潮中&#xff0c;工商业储能系统因其提升清洁能源利用率、降低电能损耗、实现“双碳”目标等优势而备受青睐。它们不仅增强了电力系统的可靠性和灵活性&#xff0c;还帮助企业降低成本、提高经济效益。储能系统通过负荷管理适应电价波动&#xff0c;…

人工智能之数学基础:线性代数在人工智能中的地位

本文重点 从本文开始&#xff0c;我们将开启线性代数的学习&#xff0c;在线性代数中有向量、矩阵&#xff0c;以及各种性质&#xff0c;那么这些数学知识究竟和人工智能有什么关系呢&#xff1f; 重要性 机器学习和深度学习的本质就是训练模型&#xff0c;要想训练模型需要使…