【字节跳动青训营】后端笔记整理-3 | Go语言工程实践之测试

简介: 用于验证已经修改或新增功能后,软件的既有功能是否受到影响。

**本人是第六届字节跳动青训营(后端组)的成员。本文由博主本人整理自该营的日常学习实践,首发于稀土掘金:🔗Go语言工程实践之测试 | 青训营


一、概述


测试主要包括:回归测试、集成测试、单元测试。


1、回归测试


用于验证已经修改或新增功能后,软件的既有功能是否受到影响。


它主要用于确保软件在经过修改后仍然能正确运行,并且新的更改没有引入新的错误或破坏原有的功能。


比如开发抖音产品,那么回归测试就是新增了一部分功能后,由负责质量保证的部门手动在终端创造一些特定的场景(比如刷一下抖音,看一下评论等)来检查功能是否正确。



2、集成测试


用于验证多个模块或组件在一起协同工作时的正确性。


它主要用于检查不同模块之间的交互和接口是否正常,以确保整个软件系统在集成后能够正常运行。在软件开发过程中,通常会将软件系统划分为多个模块或组件,每个模块负责实现不同的功能。


在集成测试中,这些模块会被组合在一起,并进行全面的测试,以验证它们之间的协作和接口是否正确。


3、单元测试


在开发阶段,开发者对单独的函数模块进行功能测试。单元测试用于验证程序中的各个独立模块(通常是函数或方法)是否按照预期进行工作。它是在软件开发中最小的测试单位,旨在测试代码的最小功能单元,以确保每个单元都能正确地完成其预定的功能。




层级从上到下,测试的覆盖率逐层变大,成本逐层降低。因此可以说,单元测试的覆盖率一定程度上决定了代码的质量。


单元测试的成本较低,覆盖率较高,可以确保每个单元都能正确工作。

集成测试的成本适中,覆盖率较低,主要用于验证不同模块之间的协作。

回归测试的成本较高,主要用于验证整个系统的稳定性和功能性。

在实际开发中,这三种测试方法通常会结合使用,以便在不同的层次和阶段上确保软件的质量和稳定性。


二、单元测试


1、流程


单元测试主要包括输入、测试单元、输出以及校对。



单元测试的流程


“单元”的概念比较广,包括接口、函数、模块等等。

用最后的校对来保证代码的功能与我们预期的相符。最后通过输出和期望值作校对,来验证代码的正确性。

单元测试一方面可以保证质量。每次编写新代码并加入了单元测试,在代码整体覆盖率足够的情况下一方面保证了新功能本身的正确性,又未破坏原有的正确性;另一方面可以提升效率,在代码有bug的情况下通过编写单测,可以在一个较短的周期内定位和修复问题。


2、规则


(1)所有的测试文件都以 _test.go 结尾。这样可以很容易辨别哪些文件是 go 的源代码,哪些是测试代码。



(2)单元测试函数的命名规范,以Test开头,且Test后面的第一个字母大写。如TestDemo。





(3)单元测试提供了一个TestMain函数,TestMain是 Go 语言中测试包(testing)的一个特殊函数,用于在运行测试之前和之后执行一些初始化和收尾操作。它不是针对单个测试用例的,而是整个测试包的入口函数。它可以被用来替代测试函数的默认入口点 func TestXxx(t *testing.T)。


TestMain的代码结构如下,它会在所有测试函数执行之前运行,并且可以在其中进行全局初始化和清理操作。然后,它会调用 m.Run() 启动所有测试函数的执行。


因为它是测试包的入口函数,所以只会执行一次。





需要注意的是,测试函数是并行执行的,所以不能依赖测试函数之间的执行顺序。


3、单元测试的例子


先在demo2.go文件中,创建一个HelloTom函数,预期返回"Tom"。

(这里为了测试,故意写成返回"Jerry"。)


//demo2.go
 
package main
 
func HelloTom() string {
  return "Jerry"
}


然后生成测试文件: demo2_test.go。

Tips:如果使用 GoLand 为IDE,快捷键 alt+insert ,然后选择Test file,可以直接生成一个单元测试文件。





在demo2_test.go中编写以下测试代码:


package main
 
import "testing"
 
func TestHelloTom(t *testing.T) {
  output := HelloTom()  //运行HelloTom 接收返回值
  expectOutput := "Tom" //预期输出Tom
  if output != expectOutput {
    t.Errorf("Expected %s do not match actual %s", expectOutput, output)
  }
}



通过 if 的方式校对 output 是否与 expected 相等。如果相等,则测试PASS,不相等则FAIL。


除了直接用运算符来判断,还有更加简便的方式,即使用 assert。


4、assert


除了使用运算符进行校对,也有很多开源的assert包可以帮助我们实现预期和实际输出equal或not equal的比较。


导入开源的assert包:


github.com/stretchr/testify/assert


可以在终端使用以下命令获取依赖(如果没有下载过,直接引入包会报红):


​go get github.com/stretchr/testify/assert


调用 assert.Equal来对预期和实际输出进行比较。第二个参数是预期输出,第三个参数是实际输出:


package main
 
import (
  "github.com/stretchr/testify/assert" // 导入开源的assert包
  "testing"
)
 
func TestHelloTom(t *testing.T) {
  output := HelloTom()  //运行HelloTom 接收返回值
  expectOutput := "Tom" //预期输出Tom
  //if output != expectOutput {
  //  t.Errorf("Expected %s do not match actual %s", expectOutput, output)
  //}
 
  assert.Equal(t, expectOutput, output)    //直接调用包中的接口
}


运行结果:actual 和 expected 不符合,测试FAIL。





若将HelloTom模块返回的值改为Tom,再次执行单元测试:




可见此时单元测试的结果是PASS。



5、覆盖率


在我们进行单元测试时需要考虑以下问题:


如何衡量代码是否经过了足够的测试?如何评价项目的测试水准?如何评估项目是否达到了高水准测试等级?


单元测试的主要评估标准是代码覆盖率,覆盖率越高则证明越多的代码经过了测试。来看下面这个例子:


创建demo3.go,其中编写了一个判断分数是否及格的功能。


package main
 
func judgePass(score int) bool {
  //return score >= 60
  if score >= 60 {
    return true
  }
  return false
}

生成测试文件demo3_test.go,调用 judgePass 并传入一个70,将结果与true校对。


package main
 
import (
  "github.com/stretchr/testify/assert"
  "testing"
)
 
func Test_judgePass(t *testing.T) {
  isPass := judgePass(70)
  assert.Equal(t, true, isPass)
}


然后运行带有覆盖率(coverage)的测试。可以用命令单独测试某个模块(如demo3.go):


go test demo3_test.go demo3.go --cover

也可以在GoLand直接操作,点击右上角的run with coverage进行测试。测试工具会自动运行测试用例,并在执行过程中跟踪被执行的代码行数。最后,它会计算测试覆盖率并给出结果。(注意,这样执行的是该目录下的所有测试文件,给出的也是整个包的测试覆盖率结果。)




最终的代码覆盖率为66.7%。这是因为demo3.go中的3条语句在case为70时执行2条。



如果在 judgePass 中直接写成 return score >= 60,那么代码的覆盖率就会变成100%。因为这个函数中所有的语句都被执行过了。


特别注意:直接点击run with coverage,执行的是该目录下的所有测试文件,给出的也是整个包的测试覆盖率结果。如果该目录下还有其它文件代码,执行测试用例后会发现覆盖率降低:




如何提升覆盖率?


刚才只是测试了70,覆盖率只有66.7%,还有部分代码没有被运行。我们希望提升覆盖率,因此可以多传入一些测试用例case。比如这里,再传入一个50:




coverage达到了 100%。


6、依赖


实际项目中,测试依赖的组件可能会很复杂。比如可能依赖一些数据库,文件,cache等。这些都属于项目中的强依赖(即是一个模块对于另一个模块存在紧密的依赖关系。当一个组件的实现或功能发生变化时,会直接影响到依赖它的其他组件的正确性或稳定性)。


而单元测试的两个目标是幂等和稳定:


幂等:重复运行一个测试,其结果都是一样的。


稳定:单元测试是相互隔离的,在任何时间任何函数都可运行。


其实,直接写单元测试可能是不稳定的,因为它可能存在一些依赖,如网络等。解决这个问题,可以在单元测试时使用mock测试。




7、文件处理


可以用文件依赖来演示一下单元测试中的依赖问题。



首先创建文件 log.txt,其中内容如下:



创建demo4.go,编写函数用于实现“读取文件的第一行”这一功能。


package main
 
import (
  "bufio"
  "os"
  "strings"
)
 
func ReadFirstLine() string {
  // 打开一个文件
  open, err := os.Open("log.txt")
 
  // defer关键字,用于延迟函数的执行
  // 这里的作用是在函数 ReadFirstLine() 执行结束后即使发生错误或提前返回,
  // 也会确保文件资源 open 被及时关闭,避免资源泄漏。
  defer open.Close()
  // 判断是否发生error
  if err != nil {
    return ""
  }
 
  // 创建文件扫描器scanner
  scanner := bufio.NewScanner(open)
  for scanner.Scan() {
    // 只读取第一行的内容 返回
    return scanner.Text() // 用于获取scanner当前所在位置的文本内容
  }
  return ""
}
 
func ProcessFirstLine() string {
  // 读取文件中的行
  line := ReadFirstLine()
  // 把读到的行进行字符串替换,把11替换成00
  destLine := strings.ReplaceAll(line, "11", "00")
  return destLine
}1.


生成单元测试。同时创建log.txt文件进行文件操作。


package main
 
import (
  "github.com/stretchr/testify/assert"
  "testing"
)
 
func Test(t *testing.T) {
  firstLine := ProcessFirstLine()
  assert.Equal(t, "line00", firstLine) // 别忘了第二个参数是expected,第三个是actual
}


运行:




这里对文件 log.txt 的依赖就是强依赖。


一旦文件被别人篡改,那测试文件可能也会受到影响,甚至无法执行。这样就无法达到单元测试幂等、稳定的目标。


三、Mock测试


Mock(模拟)是一种测试技术,用于在测试代码时替代某些依赖项或功能,以便进行可控制和可预测的测试。这种技术的目的是模拟真实环境中的特定行为,从而使开发人员能够对软件的不同部分进行独立测试,而不需要依赖其他组件的完整性或稳定性。


打桩(Stubbing)是Mock技术的一种应用。它是在单元测试中使用模拟对象(通常称为“桩”或“stub”)代替真实的依赖项或功能,以模拟这些依赖项或功能的行为。打桩的目的是在测试代码的过程中隔离被测试代码,并使测试更简单、可控、可重复和高效。


举个例子,假设有一个函数A,它依赖于函数B的返回结果。在单元测试函数A时,我们可以打桩函数B的行为,使其返回我们预先设定好的值,而不是实际去调用函数B。这样就能够独立地测试函数A的逻辑,而不必担心函数B的实际行为。


演示一下对 demo4.go 中的 ReadFirstLine 进行打桩测试,不再依赖本地文件:


首先引入monkey:bou.ke/monkey


monkey是一个开源的mock测试库,可以对函数或方法进行mock。这是monkey中的打桩函数和卸载打桩函数:




我们就调用它们来实现mock。在测试文件中编写以下代码:


package main
 
import (
  "bou.ke/monkey"
  "github.com/stretchr/testify/assert"
  "testing"
)
 
func TestProcessFirstLine(t *testing.T) {
  // 调用打桩函数 打桩函数实现了mock的功能
  // 对ReadFirstLine进行打桩操作,替换函数为输出“line110”
  monkey.Patch(ReadFirstLine, func() string {
    return "line11"
  })
  // 使用defer完成打桩函数的卸载
  defer monkey.Unpatch(ReadFirstLine)
  // 再次获取函数的返回值
  firstLine := ProcessFirstLine()
  // 通过mock,就避免了对log.txt的强依赖。这个单元测试可以在任何时间任何环境去执行
  assert.Equal(t, "line00", firstLine)
}


调用打桩函数,模拟ReadFirstLine函数的功能。这样,在该测试模块中并没有实际调用ReadFirstLine函数,也没有用到 log.txt ,但是也能完成测试。而且不会因为文件遭到破坏而无法验证。


四、基准测试


go中也提供了基准测试的框架。在 Go 语言中,基准测试用于衡量代码的性能(如运行性能和cpu损耗),特别是在处理大量数据时的性能表现。在实际中,当遇到代码性能瓶颈时,为了定位问题经常要对代码做性能分析,这就用到了基准测试。基准测试的使用方法类似于单元测试。


创建demo5.go,这是一个模拟随机选择服务器的程序:



package main
 
import (
  "testing"
)
 
func BenchmarkSelect(b *testing.B) {
  InitServerIndex()
  b.ResetTimer() //定时器重置,因为函数InitServerIndex不属于要测试的函数的损耗,所以计时要把这个时间去掉
  for i := 0; i < b.N; i++ {
    Select()
  }
}
 
// 基准测试也支持并行
func BenchmarkSelectParallel(b *testing.B) {
  InitServerIndex()
  b.ResetTimer()
  b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
      Select()
    }
  })
}


编写测试文件,下面写了两个基准测试函数:


package main
 
import (
  "testing"
)
 
func BenchmarkSelect(b *testing.B) {
  InitServerIndex()
  b.ResetTimer() //定时器重置,因为函数InitServerIndex不属于要测试的函数的损耗,所以计时要把这个时间去掉
  for i := 0; i < b.N; i++ {
    Select()
  }
}
 
// 基准测试也支持并行
func BenchmarkSelectParallel(b *testing.B) {
  InitServerIndex()
  b.ResetTimer()
  b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
      Select()
    }
  })
}函数实现了mock的功能
  // 对ReadFirstLine进行打桩操作,替换函数为输出“line110”
  monkey.Patch(ReadFirstLine, func() string {
    return "line11"
  })
  // 使用defer完成打桩函数的卸载
  defer monkey.Unpatch(ReadFirstLine)
  // 再次获取函数的返回值
  firstLine := ProcessFirstLine()
  // 通过mock,就避免了对log.txt的强依赖。这个单元测试可以在任何时间任何环境去执行
  assert.Equal(t, "line00", firstLine)
}

BenchmarkSelect:这个基准测试函数用于测试 Select 函数的性能。在基准测试开始之前,调用 InitServerIndex 函数进行初始化工作。然后,通过循环运行 Select 函数 b.N 次,b.N 表示测试运行的迭代次数。


BenchmarkSelectParallel:这个基准测试函数也用于测试 Select 函数的性能,不同之处在于它使用了并行测试。通过 b.RunParallel 函数,我们可以在多个 goroutine 中并行运行 Select 函数。这样可以更好地利用多核处理器的性能,加快测试的执行速度。


运行结果:




基准测试也支持并行执行,但是可见并行去做基准测试的情况下,它的性能退化了。原因是Select中用到了rand函数,而rand为了保证全局的随机性和并发安全,它持有全局锁。因此就降低了并发的性能。

用fastrand可以提升性能。后期如果有随机场景,推荐使用fastrand。






相关文章
|
6天前
|
API 持续交付 开发者
后端开发中的微服务架构实践与挑战
在数字化时代,后端服务的构建和管理变得日益复杂。本文将深入探讨微服务架构在后端开发中的应用,分析其在提高系统可扩展性、灵活性和可维护性方面的优势,同时讨论实施微服务时面临的挑战,如服务拆分、数据一致性和部署复杂性等。通过实际案例分析,本文旨在为开发者提供微服务架构的实用见解和解决策略。
|
2天前
|
消息中间件 设计模式 运维
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在现代后端开发中的应用,通过实际案例分析,揭示了其在提升系统灵活性、可扩展性及促进技术创新方面的显著优势。同时,文章也未回避微服务实施过程中面临的挑战,如服务间通信复杂性、数据一致性保障及部署运维难度增加等问题,并基于实践经验提出了一系列应对策略,为开发者在构建高效、稳定的微服务平台时提供有价值的参考。 ####
|
3天前
|
测试技术 Go
go语言中测试工具
【10月更文挑战第22天】
14 4
|
3天前
|
消息中间件 监控 数据管理
后端开发中的微服务架构实践与挑战####
【10月更文挑战第29天】 在当今快速发展的软件开发领域,微服务架构已成为构建高效、可扩展和易于维护应用程序的首选方案。本文探讨了微服务架构的核心概念、实施策略以及面临的主要挑战,旨在为开发者提供一份实用的指南,帮助他们在项目中成功应用微服务架构。通过具体案例分析,我们将深入了解如何克服服务划分、数据管理、通信机制等关键问题,以实现系统的高可用性和高性能。 --- ###
22 2
|
5天前
|
存储 监控 NoSQL
构建高效后端服务:从理论到实践
【10月更文挑战第30天】在数字化时代,后端服务是支撑起整个互联网的基石。一个高效、稳定且可扩展的后端系统对于任何在线业务都是至关重要的。本文将带你了解如何从零开始构建一个高效的后端服务,涵盖了设计思路、关键技术选型、开发流程以及性能优化等方面。我们将通过实际的代码示例和案例分析,深入探讨如何实现一个既快速又可靠的后端系统。无论你是后端开发的新手还是有经验的开发者,这篇文章都将为你提供宝贵的参考和启示。
22 3
|
1天前
|
缓存 API 持续交付
构建高效后端服务的实践之路
【10月更文挑战第34天】在数字化时代的浪潮中,后端服务是支撑整个互联网的脊梁。本文将深入探讨如何构建一个高效的后端服务,涵盖从选择合适的编程语言和框架,到设计高性能的数据库交互、实现缓存机制、优化API设计,以及确保服务的高可用性和可扩展性。我们将通过实际代码示例,展示如何在面对复杂业务逻辑时,依然能够保持后端服务的高效与稳定。无论你是后端开发的新手,还是希望提升现有服务性能的资深开发者,这篇文章都将为你提供宝贵的实践经验和启示。
|
2天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
6天前
|
NoSQL 测试技术 Go
自动化测试在 Go 开源库中的应用与实践
本文介绍了 Go 语言的自动化测试及其在 `go mongox` 库中的实践。Go 语言通过 `testing` 库和 `go test` 命令提供了简洁高效的测试框架,支持单元测试、集成测试和基准测试。`go mongox` 库通过单元测试和集成测试确保与 MongoDB 交互的正确性和稳定性,使用 Docker Compose 快速搭建测试环境。文章还探讨了表驱动测试、覆盖率检查和 Mock 工具的使用,强调了自动化测试在开源库中的重要性。
|
7天前
|
设计模式 人工智能 API
后端开发中的微服务架构实践与挑战#### 一、
本文将深入浅出地探讨微服务架构在后端开发中的应用实践,分析其带来的优势与面临的挑战。通过具体案例,展示如何有效地构建、部署和管理微服务,旨在为读者提供一份实用的微服务架构实施指南。 #### 二、
|
8天前
|
Go
Go语言中的并发编程:深入探索与实践###
探索Go语言的并发编程,就像解锁了一把高效处理复杂任务的钥匙。本文旨在通过简明扼要的方式,阐述Goroutines和Channels如何协同工作,以实现高效的并发处理。不同于传统的技术文档,这里我们将用一个生动的故事来串联起这些概念,让你在轻松阅读中领悟到并发编程的精髓。 ###