Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一文帮你解决单元测试中的所有疑问 #207

Closed
WGrape opened this issue Jun 27, 2022 · 0 comments
Closed

一文帮你解决单元测试中的所有疑问 #207

WGrape opened this issue Jun 27, 2022 · 0 comments
Labels
Devops Go常见问题系列 分享在工作中常遇到的一些Go语言问题 经验之谈系列 分享一些常见的技术问题和经验之道

Comments

@WGrape
Copy link
Owner

WGrape commented Jun 27, 2022

前言

本文原创,著作权归WGrape所有,未经授权,严禁转载

目录

一、什么是单元测试

在项目流程中占据重要位置的测试流程,通常是指在开发流程结束后进行的一次规范化、系统化的全面测试,这个测试工作一般是由专门的测试人员(QA)完成的,它是产品上线前的最后一道安全保障。

但随着技术的发展,人们越来越意识到一个问题,测试人员虽然可以使用接口测试、测试覆盖率、自动化测试等各种更先进的测试方式,但本质上都只能从用户的使用角度去测试,而用户的使用行为是无法完全枚举出来的。这就意味着只依赖测试人员的测试,一定是会存在有Bug风险的。

所以为了进一步提高测试质量,就产生了一种从代码角度去测试的方法,它以函数作为基本的测试单元,并在输入特定的Case后,通过比较输出是否符合预期,来表示此单元是否通过测试,这就是单元测试。

二、单元测试的重要性

在一般的项目流程中,开发完成后,测试才会介入开始工作。这种流程下,经常会遇到开发质量较低,导致测试工作被阻塞甚至要求打回重新开发的问题。

由此产生了一种称为测试驱动开发(Test-Driven Development :TDD)的开发方式,它要求在开发项目的同时,也必须编写单元测试代码。单元测试作为整个测试驱动开发中的核心,旨在通过测试来提升代码质量,驱动开发过程。

三、遵守首要原则

1、AIR原则

在宏观上,AIR原则定义了单元测试的意义就像是空气一样,虽然在线上环境看不到它的存在,但它却是线上安全的必要保障

  • Automatic(自动化):单元测试是项目的一种强制性约束,必须是完全自动化的,而且必须随着项目的进行而自动执行,否则就会失去单元测试的意义
  • Independent(独立性):单元测试过程中必须使用断言去验证单元测试的正确性,不能有任何人为参与的过程,比如最常见的 Print打印操作都是错误的
  • Repeatable(可重复):单元测试的整个生命周期都与项目周期同在,在期间会被执行无数次,为了保证单元测试的简单性和可维护性,各个测试单元之间不能存在耦合、互相调用、执行先后顺序的问题,以保证单元测试是可以被重复执行且无任何影响的

2、BCDE原则

在微观上,BCDE原则规定了如何编写一个合格的单元测试

  • B: Border,指边界值测试,如特殊值、循环边界、时间边界等
  • C: Correct,指输入正确的值,并得到预期的结果
  • D: Design,指编写单元测试的过程,需要与开发设计文档相结合
  • E: Error,指的是单元测试目标是证明程序有错,而不是证明程序无错

四、如何编写单元测试

1、创建TestMain

每一个Package下,都需要有一个TestMain函数,这个TestMain函数可以写在此Package下的任何 _test.go 文件中。通常在TestMain函数中会完成各种配置初始化相关的操作,并通过m.Run()自动执行所有Test*单元测试。

func TestMain(m *testing.M) {

    // config初始化相关
    gopath := os.Getenv("GOPATH")
    if gopath == "" {
        gopath = build.Default.GOPATH
    }
    cnofigFile := gopath + "/src/project/config/config.toml"
    initConfig(cnofigFile)

    // 一定要执行这个,否则会提示找不到测试
    m.Run()
}

2、创建单元测试文件

创建一个以某个具体go文件名为前缀,_test.go为后缀的文件即表示创建了一个单元测试文件。如现在有一个relation.go文件,再创建一个对应的relation_test.go文件即表示定义了一个relation.go的单元测试文件。

3、创建单元测试函数

单元测试函数需要在单元测试文件中创建,单元测试函数的名称通常以Test为前缀,且必须使用大驼峰命名。

func TestCompare(t *testing.T){
}

4、单元测试结果

每个单元的测试结果都是通过或失败,当检测到实际结果与预期值不符合的时候,可以使用t.Fail()来标识测试失败未通过。

func TestCompare(t *testing.T){
    if Compare(10, 20) != -1 {
        t.Fail()
    }
}

5、使用断言

在实际进行单元测试的时候,为了判断实际值与预期值是否一致,可能需要写很多丑陋的if条件语句,出现很多冗余代码。为了解决这个问题,可以使用断言。

断言是单元测试中用于判断结果是否符合预期的一种高效工具,Go语言本身不提供断言库,如果需要可以使用testify(https://github.com/stretchr/testify)库

func TestCompare(t *testing.T){
    assert := assert.New(t)
    
    assert.NotEqual(-1, Compare(10, 20), "Compare error")
}

五、场景需求

1、测试顺序控制

虽然不建议测试有执行顺序的依赖,但有时候对于需要控制测试顺序的场景,一般可以定义TestAll函数(函数名任意即可),充当协调工作,通过控制 t.Run 的顺序来控制单元测试的顺序。

package match

import (
	"container/heap"
	"fmt"
	"testing"
)

var workHeap = &WorkHeap{}

func testLiveHeapPush(t *testing.T) {
	heap.Push(workHeap, WorkHeapElement{
		WorkId: "id-1",
		Score:  10,
	})

	heap.Push(workHeap, WorkHeapElement{
		WorkId: "id-2",
		Score:  5,
	})
}

func testLiveHeapLen(t *testing.T) {
	if workHeap.Len() != 2 {
		t.Fail()
	}
}

func testLiveHeapPop(t *testing.T) {
	fmt.Println(workHeap)
	if heap.Pop(workHeap).(WorkHeapElement).WorkId != "id-1" {
		t.Fail()
		return
	}
}

func TestAll(t *testing.T) {
	t.Run("testLiveHeapPush", testLiveHeapPush)
	t.Run("testLiveHeapLen", testLiveHeapLen)
	t.Run("testLiveHeapPop", testLiveHeapPop)
}

2、CI/CD

一般可以在项目目录下创建test.sh脚本,在此脚本中完成对所有单元测试的运行,并将test.sh脚本集成在CI/CD环境中即可,可以参考ParseAOF项目。

3、测试覆盖率

由于达到100%测试覆盖率是比较困难的,特别是对于大工程下的每一个单元测试,所以一般不会对单元测试中的测试覆盖率做强制要求。对于测试覆盖率的使用可以参考go test coverage

六、通用规范

1、最小测试单元

在单元测试中,测试的单元越小越精简,测试的准确度也就越高。所以要尽量做到使用最小测试单元,满足不可分割性

BadGood
func TestCompare(t *testing.T){
    var (
        a = 1
        b = 2
    )
    Swap(a, b)
    if Compare(a, b) != true {
        t.Fail()
    }
}
func TestCompare(t *testing.T){
    if Compare(1, 2) != true {
        t.Fail()
    }
}

2、不依赖外部资源

在函数中经常有读取文件、数据库、Redis等外部资源的场景,对于这种情况,一定要把数据读取和数据处理这两部分解耦,测试数据处理部分即可。

BadGood
func TestIsOldUser(t *testing.T){
    uid := 18726
    user := GetUserFromDB(uid)
    if IsOldUser(user) != true {
        t.Fail()
    }
}
func TestIsOldUser(t *testing.T){
    user := User{
        age: 23,
    }
    if IsOldUser(user) != true {
        t.Fail()
    }
}
@WGrape WGrape changed the title 一文解决关于单元测试的所有问题 一文解决关于单元测试的所有疑问 Jun 29, 2022
@WGrape WGrape changed the title 一文解决关于单元测试的所有疑问 一文解决单元测试的所有疑问 Jun 29, 2022
@WGrape WGrape added Go常见问题系列 分享在工作中常遇到的一些Go语言问题 经验之谈系列 分享一些常见的技术问题和经验之道 labels Jun 29, 2022
@WGrape WGrape changed the title 一文解决单元测试的所有疑问 一文帮你解决单元测试中的所有疑问 Jun 29, 2022
@WGrape WGrape added the Devops label Aug 19, 2022
@WGrape WGrape closed this as completed Aug 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Devops Go常见问题系列 分享在工作中常遇到的一些Go语言问题 经验之谈系列 分享一些常见的技术问题和经验之道
Projects
None yet
Development

No branches or pull requests

1 participant