golang web补充知识:单元测试

文章目录

golang 单元测试

引言

Go语言Web编程进阶中,我们使用到了单元测试的概念,来对编写的程序进行测试。单元测试是软件开发过程中的一种质量保证方法,它通过对软件中的最小可测试单元进行检查和验证,确保每个单元都能按照预期工作。

单元测试的重要性
  • 提高代码质量:单元测试能够及时发现代码中的错误和缺陷,减少软件发布后出现问题的概率。
  • 简化调试过程:当发现代码异常时,单元测试可以帮助开发者快速定位问题所在,减少调试时间。
  • 促进代码重构:有了单元测试的保护,开发者可以更加自信地对代码进行重构,提高代码的可读性和可维护性。
  • 文档化代码:单元测试可以看作是代码的一种文档形式,它展示了函数或方法预期的行为和功能。
  • 支持持续集成:单元测试可以自动化运行,与持续集成工具结合使用,确保代码库的稳定性。
Go语言单元测试的优势

Go语言在设计时就非常注重简洁和高效,这同样体现在其单元测试的支持上。Go语言单元测试的优势有

  • 内置测试框架:Go语言的标准库中包含了完整的测试框架,无需额外安装第三方库。
  • 简洁的测试语法:Go语言的测试代码编写简单,易于理解,即使是初学者也能快速上手。
  • 并行测试:Go语言的测试框架支持并行执行测试,提高测试效率。
  • 测试覆盖率工具:Go语言提供了内置的测试覆盖率工具,可以方便地分析测试的全面性。
  • 集成到标准工作流:Go语言的测试与构建、部署等环节紧密集成,支持统一的命令行操作。
  • 社区支持:Go语言有着活跃的社区,提供了大量的文档、教程和工具,方便开发者学习和使用单元测试。

Go语言单元测试基础

testing包

在Go语言中,testing包是编写单元测试的基础。这个包提供了测试框架和断言函数,使得编写和运行测试变得非常简单。

  • 测试函数

    在Go中,测试代码通常位于与被测试代码相同的包中,测试文件通常以_test.go结尾(如hello_test.go)。测试函数的命名规则是以Test开头,后跟被测试函数的名字,例如TestAdd用于测试Add函数。

    go 复制代码
    func TestAdd(t *testing.T) {
        sum := Add(1, 2)
        if sum != 3 {
            t.Errorf("Add(1, 2) = %d; want 3", sum)
        }
    }

    **说明:***testing.T是一个类型,它代表着一个测试的上下文。当一个测试函数被执行时,它会被传递给测试函数,以便测试函数可以使用它来报告测试的状态、记录日志、报告错误或者失败

  • 测试组

    测试组允许你将相关的测试函数组合在一起,通常用于共享相同的设置代码

    go 复制代码
    func TestGroup(t *testing.T) {
        tests := []struct {
            name string
            x    int
            y    int
            want int
        }{
            {"Positive", 1, 2, 3},
            {"Negative", -1, -1, -2},
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                got := Add(tt.x, tt.y)
                if got != tt.want {
                    t.Errorf("Add(%d, %d) = %d; want %d", tt.x, tt.y, got, tt.want)
                }
            })
        }
    }

    在这段代码中,我们定义了一个名为TestGroup的测试函数,它包含了一个结构体切片tests,这个切片包含了多个测试用例。每个测试用例都有一个名字(name)和对应的输入值(x和y)以及期望的输出值(want)

    接着,我们遍历这个tests切片,并使用t.Run方法来执行每个测试用例。t.Run的第一个参数是子测试的名字,它将显示在测试输出中;第二个参数是一个函数,这个函数包含了实际的测试代码

    t.Run的函数参数中,我们调用了Add函数,并将实际的结果(got)期望的结果(tt.want)进行比较。如果它们不相等,我们使用t.Errorf来报告测试失败,并输出错误信息。

    通过这种方式,我们可以将一组相关的测试用例组织在一起,每个测试用例都可以独立运行,同时共享TestGroup函数中的设置代码。这使得测试更加模块化,易于管理和维护。

辅助测试函数

testing包中,测试辅助函数是一组用于在测试过程中报告状态、错误、日志等的方法。这些方法都是*testing.T类型的一部分,下面是一些常用的测试辅助函数及其例子

  1. Error和Errorf

    Error和Errorf方法用于在测试中报告错误。它们会标记测试为失败,但不会停止测试的执行。

    go 复制代码
    func TestAdd(t *testing.T) {
        sum := Add(1, 2)
        if sum != 3 {
            t.Error("Add(1, 2) failed") // 使用Error
        }
    }
    
    func TestSubtract(t *testing.T) {
        result := Subtract(5, 3)
        if result != 2 {
            t.Errorf("Subtract(5, 3) = %d; want 2", result) // 使用Errorf
        }
    }
  2. Fail和FailNow

    Fail方法用于标记测试为失败,但不提供错误信息。FailNow方法会立即停止当前测试的执行,并标记测试为失败

    go 复制代码
    func TestFail(t *testing.T) {
        t.Fail() // 标记测试为失败,但继续执行
        // 更多代码...
    }
    
    func TestFailNow(t *testing.T) {
        t.FailNow() // 立即停止测试
        // 此行代码不会被执行
    }
  3. Fatal和Fatalf

    Fatal和Fatalf方法用于在测试中报告致命错误。它们会标记测试为失败,并立即停止测试的执行。

    go 复制代码
    func TestFatal(t *testing.T) {
        if _, err := os.Stat("nonexistentfile"); err == nil {
            t.Fatal("os.Stat returned no error for nonexistent file") // 使用Fatal
        }
    }
    
    func TestFatalf(t *testing.T) {
        if _, err := os.Open("nonexistentfile"); err != nil {
            t.Fatalf("os.Open returned an error: %v", err) // 使用Fatalf
        }
    }
  4. Log和Logf

    Log和Logf方法用于记录测试日志。这些日志信息仅在详细输出时可见,不会影响测试的状态。

    go 复制代码
    func TestLog(t *testing.T) {
        t.Log("This is a log message") // 使用Log
    }
    
    func TestLogf(t *testing.T) {
        t.Logf("This is a formatted log message: %v", 42) // 使用Logf
    }
  5. Skip和Skipf

    Skip和Skipf方法用于跳过当前测试。这些方法通常用于条件测试,当某些前提条件不满足时,可以跳过测试

    go 复制代码
    func TestSkip(t *testing.T) {
        if os.Getenv("SKIP_TEST") == "true" {
            t.Skip("Skipping this test due to environment variable") // 使用Skip
        }
        // 测试代码...
    }
    
    func TestSkipf(t *testing.T) {
        if runtime.GOOS == "windows" {
            t.Skipf("Skipping this test on Windows") // 使用Skipf
        }
        // 测试代码...
    }
运行单元测试

运行单元测试通常可以通过IDE工具或者命令行的方式来触发,下面展示一些运行的例子

  1. IDE的以vscode为例子

    可以点击run test(直接运行)或者debug test(debug代码)的方式,来对测试代码进行验证
  2. 命令行
    • 测试全部测试文件

      最基础的运行单元测试的方法是使用go test命令。在你的项目根目录或包含测试文件的目录下,运行以下命令

      sh 复制代码
      go test

      这会编译并运行所有命名符合TestXXX格式的测试函数。

    • 测试单个文件

      sh 复制代码
      go test -run TestFileName

      其中TestFileName是你的测试文件名(不包括.go扩展名)

    • 测试单个测试函数

      可以通过指定测试函数的全名来运行单个测试函数

      sh 复制代码
      go test -run TestFunctionName

      这里TestFunctionName是你要运行的测试函数的名称

    • 并行运行测试

      如果你的测试是并行安全的,你可以使用-parallel标志来并行运行测试,以提高效率

      sh 复制代码
      go test -parallel 4

      这里4是并行运行的测试数量。你可以根据你的CPU核心数来调整这个值。

    • 显示测试覆盖率

      可以使用-cover标志来查看测试覆盖率

      sh 复制代码
      go test -cover

      这将输出每个包的测试覆盖率摘要

    • 输出详细信息

      如果你想看到更多的测试输出,可以使用-v标志

      sh 复制代码
      go test -v

      这将显示每个测试函数的运行结果和日志信息。

    • 测试当前包

      如果你想测试当前包,而不管它是否是测试主包,可以使用-run标志和.模式

      sh 复制代码
      go test -run .

通过这些命令和选项,你可以灵活地运行和管理你的Go语言单元测试

Mock技术

Mock简介

Mock技术是在单元测试中用来模拟外部依赖项(如数据库、网络服务、文件系统等)的技术。在编写单元测试时,我们通常希望测试案例能够独立于外部系统,以便能够快速、可靠地运行测试,并确保测试结果只受被测试代码的影响。Mock技术允许我们创建模拟对象(mock objects),这些对象的行为和真实的外部依赖项相似,但在测试环境中是可控的。

其优势主要体现在

  • 隔离测试:通过模拟外部依赖,我们可以确保测试只针对当前单元,而不是整个系统。
  • 可重复性:Mock对象提供一致的行为,使得测试结果可预测和重复。
  • 速度:Mock对象通常比真实的外部系统更快,可以加快测试执行速度。
  • 安全性:Mock技术可以避免在测试过程中对真实数据库或服务进行不必要的操作,减少风险。
  • 灵活性:可以模拟各种异常情况,测试边缘情况和错误处理逻辑。

Go中手动mock的示例

假设我们有一个简单的用户服务,该服务负责从数据库中获取用户信息。我们将使用Mock技术来模拟数据库服务,以便在不需要实际数据库的情况下测试用户服务

首先,我们定义一个数据库接口和它的实现:

go 复制代码
// db/interface.go
package db

type User struct {
    ID   int
    Name string
}

type Database interface {
    GetUser(id int) (*User, error)
}
go 复制代码
// db/mock_db.go
package db

type MockDB struct {
    data map[int]*User
}

func (m *MockDB) GetUser(id int) (*User, error) {
    user, ok := m.data[id]
    if !ok {
        return nil, fmt.Errorf("user not found")
    }
    return user, nil
}

func NewMockDB() *MockDB {
    return &MockDB{
        data: make(map[int]*User),
    }
}

现在,我们有一个用户服务,它使用这个数据库接口来获取用户信息:

go 复制代码
// service/user_service.go
package service

import (
    "fmt"
    "myproject/db"
)

type UserService struct {
    db db.Database
}

func NewUserService(db db.Database) *UserService {
    return &UserService{
        db: db,
    }
}

func (us *UserService) GetUser(id int) (string, error) {
    user, err := us.db.GetUser(id)
    if err != nil {
        return "", err
    }
    return fmt.Sprintf("User: %s", user.Name), nil
}

最后,我们编写一个单元测试来测试UserService,使用Mock数据库代替真实的数据库:

go 复制代码
// service/user_service_test.go
package service

import (
    "myproject/db"
    "testing"
)

func TestGetUser(t *testing.T) {
    // 创建Mock数据库
    mockDB := db.NewMockDB()

    // 设置Mock数据
    mockDB.data[1] = &db.User{
        ID:   1,
        Name: "John Doe",
    }

    // 创建UserService实例,注入Mock数据库
    userService := NewUserService(mockDB)

    // 调用GetUser方法
    name, err := userService.GetUser(1)

    // 断言结果
    if err != nil {
        t.Errorf("Expected no error, got %v", err)
    }
    if name != "User: John Doe" {
        t.Errorf("Expected name 'John Doe', got %s", name)
    }
}

在这个例子中,我们创建了一个MockDB实例,并设置了期望的数据。然后,我们创建了UserService实例,并将MockDB注入到其中。最后,我们调用了GetUser方法,并使用断言来验证结果是否符合预期

不过这样手动mock的方法比较冗长,也不好批量编写,我们可以用GoMock框架来轻松编写mock的单元测试

GoMock框架介绍

GoMock 是一个用于 Go 语言的开源 Mock 框架,它允许开发者创建 Mock 对象来模拟接口的行为。GoMock 是由 Go 社区维护的,并且与 Go 的标准测试包 testing 集成得很好。使用 GoMock,可以轻松地为接口创建 Mock 实现,并定义这些 Mock 对象在调用时的行为和返回值

特点

  • 基于接口的 Mocking:GoMock 允许你为任何接口生成 Mock 实现。
  • 强大的断言:可以精确地断言 Mock 方法被调用的次数、参数和返回值。
  • 灵活的行为定义:可以定义 Mock 方法在每次调用时的不同行为。
  • 与 Go 测试包集成:GoMock 与 Go 的标准测试包 testing 完美集成,可以很容易地在测试中使用 Mock 对象。

安装 GoMock

要使用 GoMock,首先需要安装 GoMock 命令行工具。可以通过以下命令安装:

sh 复制代码
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen@latest

google在2023年已经停止了对gomock的更新和支持,以上的命令也还能获取,如果要获取更新的版本,可以用下面的

sh 复制代码
go get github.com/uber-go/mock/gomock
go install go.uber.org/mock/mockgen@latest
编写Mock代码

使用 GoMock 的基本步骤

  1. 定义接口:首先,你需要定义一个接口,你想要为这个接口创建 Mock 实现

    go 复制代码
    // my_interface.go
    package mypackage
    
    type MyInterface interface {
        DoSomething(arg int) (int, error)
    }
  2. 生成 Mock 代码:使用 GoMock 命令行工具mockgen生成 Mock 代码

    sh 复制代码
    mockgen -source=my_interface.go -destination=my_interface_mock.go -package=mypackage

    这将生成一个名为 my_interface_mock.go 的文件,其中包含 Mock 实现的代码。

  3. 编写测试:在你的测试代码中,创建 Mock 控制器并定义 Mock 对象的行为

    go 复制代码
    // my_interface_test.go
    package mypackage
    
    import (
        "github.com/golang/mock/gomock"
        "testing"
    )
    
    func TestMyFunction(t *testing.T) {
        ctrl := gomock.NewController(t)
        defer ctrl.Finish()
    
        mock := NewMockMyInterface(ctrl)
        mock.EXPECT().DoSomething(gomock.Eq(123)).Return(456, nil)
    
        // 使用 Mock 对象进行测试
    }
    1. 代码首先创建了一个新的 Mock 控制器ctrl。控制器负责管理 Mock 对象和 Mock 期望(expectations)。
    2. defer ctrl.Finish() 是一个延迟调用的声明,它将在当前函数执行完毕后执行。Finish 方法是 Mock 控制器的一个方法,它用于释放控制器占用的资源,比如在测试结束后关闭 Mock 对象可能打开的文件描述符。
    3. mock := NewMockMyInterface(ctrl) 创建了一个新的 Mock 对象,它实现了我们之前定义的 MyInterface 接口。NewMockMyInterface 是 GoMock 框架提供的一个方法,它创建了一个新的 Mock 对象,并将其与当前的 Mock 控制器关联起来。
    4. mock.EXPECT().DoSomething(gomock.Eq(123)).Return(456, nil) 定义了一个期望的行为。EXPECT() 方法用于告诉 GoMock,我们应该期望 DoSomething 方法被调用,并且它应该被调用的方式。gomock.Eq(123) 是一个断言,它告诉 GoMock,我们期望 DoSomething 方法的第一个参数等于 123Return(456, nil) 定义了当 DoSomething 方法被调用时,我们应该返回的值。在这个例子中,我们期望 DoSomething 方法被调用一次,并且它的第一个参数是 123,然后我们应该返回 456nil
使用Mock进行单元测试

我们看一个具体的例子,创建一个简单的 HTTP 客户端接口和它的 Mock,然后编写一个测试用例来使用这个 Mock

  1. 定义接口:首先,定义一个 HTTP 客户端接口

    go 复制代码
    // http_client/interface.go
     package httpclient
    
     type HTTPClient interface {
         Get(url string) ([]byte, error)
     }
  2. 生成 Mock 实现:使用 mockgen 命令生成 Mock 实现。

    sh 复制代码
    mockgen -source=interface.go -destination=interface_mock.go -package=mypackage

    有兴趣的同学可以对比一下,看看interface_mock.go与之前手动编写的mock_db.go有什么区别,来加深对于mock的设计理念的认识

  3. 编写一个使用 HTTP 客户端的服务

    go 复制代码
    // service/http_service.go
    package service
    
    import (
        "encoding/json"
    
        httpclient "golang-30-days/Day13-15/code/unit_test/http_client"
    )
    
    type Response struct {
        Message string `json:"message"`
    }
    
    func FetchMessage(client httpclient.HTTPClient, url string) (string, error) {
        data, err := client.Get(url)
        if err != nil {
            return "", err
        }
    
        var resp Response
        if err := json.Unmarshal(data, &resp); err != nil {
            return "", err
        }
    
        return resp.Message, nil
    }
  4. 编写一个单元测试,使用 GoMock Mock 对象来模拟 HTTP 客户端的行为

    go 复制代码
    // service/http_service_test.go
     package service
    
     import (
         "encoding/json"
         httpclient "golang-30-days/Day13-15/code/unit_test/http_client"
         "testing"
    
         "github.com/golang/mock/gomock"
     )
    
     func TestFetchMessage(t *testing.T) {
         // 创建 Mock 控制器
         ctrl := gomock.NewController(t)
         defer ctrl.Finish()
    
         // 创建 Mock HTTP 客户端
         mockClient := httpclient.NewMockHTTPClient(ctrl)
    
         // 设置 Mock 行为:当调用 Get 方法时,返回预定义的响应
         mockResponse := Response{Message: "Hello, World!"}
         mockData, _ := json.Marshal(mockResponse)
         mockClient.EXPECT().Get("http://example.com/message").Return(mockData, nil)
    
         // 调用服务函数
         message, err := FetchMessage(mockClient, "http://example.com/message")
    
         // 断言结果
         if err != nil {
             t.Errorf("Expected no error, got %v", err)
         }
         if message != mockResponse.Message {
             t.Errorf("Expected message '%s', got '%s'", mockResponse.Message, message)
         }
     }

    可以看到我们并没有直接调用我们进行mock的接口,而是通过FetchMessage方法,来间接调用httpclient.Get的;我们并未对FetchMessage进行mock操作,但最后返回的依然是我们设定的返回结果,而不是实际去请求http://example.com/message。由此可见,mock控制器创建后,就会直接在测试函数域内生效,只要使用httpclient.Get接口,并且输入参数为http://example.com/message,都会返回我们预设的{Message: "Hello, World!"}

面向测试编程(TDD)

TDD简介

面向测试编程(Test-Driven Development,TDD)是一种软件开发过程,它强调在编写实际的代码实现之前先编写测试代码。这种开发方式的核心思想是通过测试来推动软件设计,确保代码的质量和可维护性

TDD的优势
  1. 提高代码质量:通过持续的测试,可以及早发现和修复错误。
  2. 更好的设计:测试驱动的方式鼓励更好的设计和模块化,因为代码必须易于测试。
  3. 文档化:测试本身可以作为代码行为的文档。
  4. 勇气重构:因为有测试作为安全保障,开发者在进行重构时更加大胆。
TDD的基本步骤

TDD通常遵循以下步骤:

  • 编写测试(Red):开发人员首先编写一个会失败的测试用例,这个测试用例用来描述代码的一个小的、特定的行为。
  • 编写最小量的代码(Green):接着,开发人员编写足够的代码使测试通过,这个过程要求开发者只编写必要的代码,而不是一开始就实现所有的功能。
  • 重构(Refactor):最后,开发人员对已经通过测试的代码进行重构,改进代码结构,提高代码的可读性和可维护性,同时确保所有的测试仍然通过。
Go语言中的TDD实践

golang中的实践主要遵循以下步骤

  1. 编写测试:在.go文件中编写测试代码。Go语言的测试文件通常与源代码文件在同一目录下,文件名以_test.go结尾。测试函数以Test开头,并且接受一个类型为*testing.T的参数。
  2. 运行测试:使用go test命令运行测试。如果测试失败,go test会输出失败的测试信息。
  3. 编写最小量的代码:根据测试失败的原因,编写最少的代码使测试通过。
  4. 重构:一旦测试通过,对代码进行重构,以提高代码质量和可读性。
  5. 重复:重复以上步骤,每次添加一个新的测试,然后编写代码使测试通过,最后重构。

看一个简单例子,首先,创建一个名为calculator_test.go的测试文件,并编写一个测试用例

go 复制代码
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(1, 2)
    expected := 3
    if result != expected {
        t.Errorf("Add(1, 2) = %d; expected %d", result, expected)
    }
}

接着,创建一个名为calculator.go的文件,并编写一个空的Add函数:

go 复制代码
package calculator

// Add takes two numbers and returns their sum.
func Add(a, b int) int {
    return 0
}

运行go test命令,测试将会失败,因为我们的Add函数还没有实现加法功能

sh 复制代码
go test

接下来,我们回到calculator.go文件,实现Add函数

go 复制代码
package calculator

// Add takes two numbers and returns their sum.
func Add(a, b int) int {
    return a + b
}

再次运行go test命令,测试会通过。之后我们可以继续添加更多的测试用例,比如测试负数相加、零相加等,然后根据测试结果逐步完善Add函数。

这就是在Go语言中实践TDD的基本流程。通过不断地重复这个过程,你可以逐步构建起一个健壮的代码库。

相关推荐
Devil枫4 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
小袁在上班10 小时前
Python 单元测试中的 Mocking 与 Stubbing:提高测试效率的关键技术
python·单元测试·log4j
测试199810 小时前
外包干了2年,快要废了。。。
自动化测试·软件测试·python·面试·职场和发展·单元测试·压力测试
qq_1728055912 小时前
GIN 反向代理功能
后端·golang·go
__AtYou__12 小时前
Golang | Leetcode Golang题解之第535题TinyURL的加密与解密
leetcode·golang·题解
安冬的码畜日常14 小时前
【The Art of Unit Testing 3_自学笔记06】3.4 + 3.5 单元测试核心技能之:函数式注入与模块化注入的解决方案简介
笔记·学习·单元测试·jest
王解15 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
kevin_tech17 小时前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式
幺零九零零20 小时前
【Golang】sql.Null* 类型使用(处理空值和零值)
数据库·sql·golang
cookies_s_s20 小时前
Golang--DOS命令、变量、基本数据类型、标识符
golang