不同语言产品的内建单测框架和指令对比

举报
码乐 发表于 2024/01/03 09:41:46 2024/01/03
【摘要】 前言我们都知道,越早发现问题越早纠正,那么程序的成本将更小。每个语言都有针对自身代码的单元测试框架,这里对不同语言的内建框架产品简单做些对比介绍,友善提醒,本文较长。 1 go 的内建框架 1.1 调试并发程序go是 多线程的, 也就是基于消息传递的并发。多线程和基于消息的并发编程是等价的。 多线程并发模型可以容易对应到多核处理器,主流操作系统也提供了系统级的多线程支持。go 语言是基于消...

前言

我们都知道,越早发现问题越早纠正,那么程序的成本将更小。
每个语言都有针对自身代码的单元测试框架,

这里对不同语言的内建框架产品简单做些对比介绍,友善提醒,本文较长。

1 go 的内建框架

1.1 调试并发程序

go是 多线程的, 也就是基于消息传递的并发。
多线程和基于消息的并发编程是等价的。 多线程并发模型可以容易对应到多核处理器,主流操作系统也提供了系统级的多线程支持。

go 语言是基于消息并发模型的集大成者,它将基于CSP 模型的并发编程内置到语言中,通过一个go关键字 轻易启动 一个例程。 并且共享内存。

可以说 goroutine是 用户态的 ,与 系统线程不是等价的,也可以说它是一个轻量级的线程。

每个系统线程都有一个固定大小的栈,一般默认2MB,这个栈大小导致了两个问题:一个对于很多只需要很小的栈空间的线性是巨大浪费,二是对于少数需要巨大栈空间的线程又面临栈溢出的风险。

因此要么降低固定栈大小,提升空间利用率,要么增大栈大小允许更深的函数递归调用,但这无法兼有。

goroutine 以一个很小的栈启动 可能是 2KB 4KB,遇到深度递归导致栈空间不足时。
goroutine 根据需要动态伸缩栈大小。 最大可达 1GB

启动代价很小,就如调用函数一样简单,而例程之间的调度代价也很低,而伸缩空间很大,这极大促进了并发编程的流行和发展。

其调度使用半抢占式调度。调度器根据具体函数只保留必要寄存器,切换的代价比系统线程低得多。 运行时有一个 runtime.GOMAXPROCS 变量,用于控制当前允许正常非阻塞 例程的系统线程数目。

并发程序基准测试在其他语言中可能是个麻烦事情,在golang中,内建单测包提供了丰富的支持。

1.2 内置 测试框架 testing

基于xUnit套件层次结构,顺序结构 用于组织测试代码之间的结构。
那么在测试用例代码内部其一般逻辑是怎样的?

go测函数就是一个普通的go函数,仅对测试函数的函数名和函数原型有特定要求, 在测试函数TestXXX会其子测试函数 subtest, 如何执行测试逻辑并没有显式约束。

对失败与否的判断在于测试代码逻辑是否进入了包含 Error/Errorf, Fatal/Fatalf 等方法调用
一旦进入这些分支,即代表该测试失败。

不同的是 error/Errorf 并不会立刻终止当前 goroutine 的执行,还会继续执行 例程的后续测试。
而Fatal/Fatalf将立刻结束当前例程的执行。

这里介绍testing功能和代码结构。
内置的性能基准数据,它有内置的pprof 和 expvar 包支持导出导入go应用的数据,有接口或json的方式可用。

基准性能测试 形式如下

  func BenchmarkXxx(* testing.B)

被视为基准测试,并在提供其 -bench 标志时由“go test”命令执行。基准测试按顺序运行。

示例基准函数如下所示:

func BenchmarkRandInt(b *testing.B) {
    for i := 0;i < bN; i++ {
        rand.Int()
    }
}

1.2.1 基准结果 ReportMetric testing.B

ReportMetric 将“n unit”添加到报告的基准测试结果中。
如果度量是每次迭代,调用者应该除以 bN,并且按照惯例,输出的报告以“/op”结尾。

ReportMetric 覆盖任何先前报告的相同单位的值。如果 unit 是空字符串或 unit 包含任何空格,则 ReportMetric 会发生混乱。

如果单位是基准框架本身通常报告的单位(例如“allocs/op”),ReportMetric 将覆盖该指标。
将“ns/op”设置为 0 将抑制该内置指标。

如下这个 排序的例子提供了一个报告与特定算法相关的自定义基准性能度量,它之间调用包内的函数:

b.ReportMetric 它除以 b.N 得到平均值,并输出到执行报告的 /op 单元.

  • 基准测试例子

    package main
    
      import (
        "sort"
        "testing"
      )
    
      func main() {
         
        testing.Benchmark(func(b * testing.B) {
          var compares int64
          for i := 0; i < b.N; i++ {
            s := []int{5, 4, 3, 2, 1}
            sort.Slice(s, func(i, j int) bool {
              compares++
              return s[i] < s[j]
            })
          }
           
          b.ReportMetric(float64(compares)/float64(b.N), "compares/op")
        })
      }
    

    基准函数必须运行目标代码 bN 次。在基准执行期间,调整 bN 直到基准函数持续足够长的时间以可靠地计时

  • 并行基准测试 RunParallel testing.PB

    RunParallel 并行运行基准测试。它创建多个 goroutines 并在它们之间分配 bN 次迭代。
    goroutines 的数量默认为 GOMAXPROCS。

    要增加非 CPU 绑定基准测试的并行性,请在 RunParallel 之前调用 SetParallelism 。

    RunParallel 通常与 go test -cpu 标志一起使用。

    如下例子在单个对象提供 text/template.Template.Execute
    RunParallel 将长久 GOMAXPROCS 例程并分散任务到它们。 每个例程有自己的字节缓冲。

    for pb.Next() 循环体在全部例程中共被执行 b.N次

    package main
    
    import (
      "bytes"
      "testing"
      "text/template"
    )
    
    func main() {
       
      testing.Benchmark(func(b * testing.B) {
        templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
         
        b.RunParallel(func(pb * testing.PB) { 
          var buf bytes.Buffer
          for pb.Next() { 
            buf.Reset()
            templ.Execute(&buf, "World")
          }
        })
      })
    }
    

主体函数将在每个 goroutine 中运行。它应该设置任何 goroutine-local 状态,然后迭代直到 pb.Next 返回 false。它不应使用 StartTimer、StopTimer 或 ResetTimer 函数,因为它们具有全局作用。它也不应该调用 Run。

1.2.2 模糊测试 FuzzXxx testing.F 和 主要测试 testing.M

  • 模拟测试

    “go test”和测试包支持模糊测试,这是一种测试技术,其中使用随机生成的输入调用函数以查找单元测试未预料到的错误。

功能函数定义形式:

    func FuzzXxx(* testing.F)

被认为是模糊测试。

  • 主要测试 TestMain testing.M

    测试或基准测试程序有时需要在执行之前或之后进行额外的设置或拆卸。
    有时还需要控制哪些代码在主线程上运行。为了支持这些和其他情况,如果测试文件包含一个函数:

    func TestMain(m * testing.M)
    那么生成的测试将调用 TestMain(m) 而不是直接运行测试或基准测试。

    TestMain 在主 goroutine 中运行,并且可以围绕对 m.Run 的调用进行任何必要的设置和拆卸。

    m.Run 将返回一个可以传递给 os.Exit 的退出代码。如果 TestMain 返回,测试包装器会将 m.Run 的结果传递给 os.Exit 本身。

    调用 TestMain 时,flag.Parse 尚未运行。如果 TestMain 依赖于命令行标志,包括测试包的标志,它应该显式调用 flag.Parse。命令行标志始终由运行的时间测试或基准函数解析。

    TestMain 的一个简单实现是:

    func TestMain(m * testing.M) {
      // 如果 TestMain 使用标志,则在此处调用 flag.Parse()
      os.Exit(m.Run())
    }
    

TestMain 是一个低级原语,对于临时测试需求来说不是必需的,普通测试函数就足够了。

1.3 组织 testing 包

go的testing包为了给自动化测试提供支持。
它旨在与“go test”命令一起使用,该命令自动执行表单的任何功能和基准测试。

其中 Xxx 不以小写字母开头。函数名称用于标识测试例程。

在这些功能中,使用 Error、Fail 或相关方法来指示失败。

要编写新的测试套件,请创建一个名称以 _ test.go 结尾的文件,其中包含此处所述的 TestXxx 函数。
将文件放在与被测文件相同的包中。

该文件将从常规包构建中排除,但会在运行“go test”命令时包含在内。
有关更多详细信息,请运行“go help test”和“go help testflag”。

一个简单的测试函数如下所示:

func TestAbs(t * testing.T) {
    hava := Abs(-1)
    if have != 1 {
        t.Errorf("Abs(-1) = %d; need 1", hava)
    }
}

由于go 的特征,它的测试一般封装为 模块 demo_test.go --> 函数 Context, 一些常用的特殊的测试关键字 如下”

测试函数

   TestXxxx()

测试函数的 套件设置和销毁

   Setup()
   TestCase()
   TearDown()

测试套件的 设置和销毁

   Setup()
   TestSuite()
   TearDown()

包级别的 设置和销毁

   pkgSetup()
   pkgTearDown()

支持回收测试套件设置 对于 包级别的测试固件的创建和销毁有了正式的原生支持 >go 1.14

   TestMain()    

1.3.1 使用方式:

  • 运行全部测试

    go test -run ''  
    
  • 运行匹配“Foo”的顶级测试,例如“TestFooBar”。

    go test -run Foo  
    
  • 对于匹配“Foo”的顶级测试,运行匹配“A=”的子测试。

    go test -run Foo/A=

  • 对于所有顶级测试,运行匹配“A=1”的子测试。

    go test -run /A=1  
    
  • 模糊匹配“FuzzFoo”的目标

    go test -fuzz FuzzFoo   
    

** 例子:

设置测试套件

  func suiteSetup(suiteName string) func() {
      fmt.Printf("\tsetUp fixture for suite %v \n", suiteName)
      return func() {
        fmt.Printf("\ttearDown fixture for suite %v\n", suiteName)
      }
    }

设置测试用例

    func funcOneTestCase(t * testing.T) {
      fmt.Printf("\t\tExecute test:%v\n", t.Name())
    }

    func TestFuncOne(t * testing.T) {
      t.CleanUp(suiteSetUp(t.Name()))
      t.Run("testcase1", funcOneTestCase1)
    }

用例的函数

     func funcTwoTestCase(t * testing.T) {
      fmt.Printf("\t\tExecute test:%v\n", t.Name())
    }

执行清理和用例

    func TestFuncTwo(t * testing.T) {
      t.CleanUp(suiteSetUp(t.Name()))
      t.Run("testcase1", funcTwoTestCase1)
    }

包初始化

  func pkgSetUp(pkgName string) func() {
    fmt.Printf("package SetUp fixture for %v\n", pkgName)
    return func() {
      fmt.Printf("package TearDown fixture for %v\n", pkgName)
    }
  }

控制哪些代码在主例程上运行

  func TestMain(m * testing.M) {
    defer pkgSetUp("package demo_test")()
    m.Run()
  }

2 与经典方式Py对比

Python的内建框架名为unittest,它非常适合测试具有相当线性控制流的代码。
基本上是按面向对象的编程方式一步一步的假设和拆除套件。

2.1 组织方式简单介绍

py内置测试包为 unittest, 测试套件与 go 的xUnit 级别层次类似,测试包,测试模块,测试类(包括测试套件设置),测试用例。

它基于 JUnit的启发。 这个模块包含的核心框架类支持 测试用例和套件的基础架构 例如 TestCase TestSuite。
并且提供运行测试和基于文本类的执行报告(TextTestRunner) 。

一个最基础的例子,用例始终以test 开头

   #//test_module
   import unittest

  class IntegerArithmeticTestCase(unittest.TestCase):
      def testAdd(self):  
          self.assertEqual((1 + 2), 3)
          self.assertEqual(0 + 1, 1)
      def testMultiply(self):
          self.assertEqual((0 * 10), 0)
          self.assertEqual((5 * 8), 40)

  if __name__ == '__main__':
      unittest.main()

只要在 main 函数中声明了unittest.main(),这将被解析为测试模块,以下方式 执行它

    python   -m unittest test_module

2.2 实例

  • 脚手架

对应每个级别都可用有设置不同层次的套件,例如下,设置fixture,可以称之为脚手架,
在setup中 从环境中读取ip信息,以便在执行用例时做为全局的信息依据。

并在此时获得日志处理对象logger:

   class TestCase(unittest.TestCase):

        def __init__(self, method_name):
            unittest.TestCase.__init__(self, method_name)

        def setUp(self):
     
          self.ip = os.getenv('ip') 
          self.logger = logger

        def tearDown(self):

          self.logger.info("===== Teardown Section of %s =====" % self.__class__.__name__)
  • 测试套件处理

这里只有简单的 成功和失败两类,如果case有更多共性,比如校验名称长度,也可以在这里做为套件处理。

    class RpcTest(TestCase):

      def successTest(self, rpc, method='POST', jsons=None):
          resp = rpc.Request(  jsons, method)
          if resp['message'] != 'true':
              self.assertEqual(resp['message'], True, msg=(resp, True))
          else:
              self.assertEqual(resp['message'], 'true', msg=(resp, 'true'))
          return resp


      def failTest(self, rpc, params, errorCode, errorMessage=None, jsons=None, method='POST'): 
      
          resp = rpc.Request(params=params, jsons=jsons, method=method)
          if resp['code'] != errorCode:
              self.assertEqual(resp['code'], errorCode, msg=(resp, errorCode))
          else:
              self.assertEqual(resp['code'], errorCode)
          return resp
          
      def nameScopeTest(...):
         ...
  • 用例设置

在具体用例中执行套件设置时,比如开始时清理环境,DB信息设置等等。 比如在结束后清理环境,还原DB,环境信息等等。

并且其内部包,在某些特殊的场景,比如环境所属地址ip 为内部环境,不需要执行失败的校验,则可选择跳过。

   class RpcBaseTest(RpcTest):
      
      scope = 'rpc'
      scopeIp = self.ip
      condition = self.CheckScope(self.ip)

      @classmethod
      def setUpClass(cls):
           
          cls.successCode = 200
          self.logger.info("test setup")

      def testPing(self):
          self.SuccessTest(...)
      
      @unittest.skipIf(condition=condition, reason=scopeIp)
      def testPingFail(self):
          self.FailTest(...)

      @classmethod
      def setTearDown(cls):
         
         cls.ClearDB()
         self.logger.info("test tear down")

相对而言,因为py发展历史长久充分,单测包 提供的功能比较全面。 go 的功能稍微差一些,但是在性能校验中有更多支持。

执行的命令行接口指令类似于 go

python -m unittest
python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

更多的功能包括探索性测试和信号处理。

  • 探索性测试
    在 TestLoader.discover() 中实现,但也可以通过命令行使用。它在命令行中的基本用法如下:

    cd project_directory
    python -m unittest discover
    
  • 信号处理 它提供了捕获中断行为(control-C)时的选项。
    因此允许测试继续并报告结果,但是多次中断将退出执行。

    python -m unittest -c/--catch 
    

    该命令行选项。 它提供了测试运行期间处理 control-C 的更友好方式。

小结

综上,在python中 单元测试 unittest是一种经典的实现方式。
而go更注重并发性能的调试,也是其优点。

  • 在py中unittest包括以下主要信息:

1 脚手架:test fixture

   test fixture 表示为了开展一项或多项测试所需要进行的准备工作脚手架,以及所有相关的清理操作。
   举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。

2 用例:test case

    一个测试用例是一个独立的测试单元。它检查输入特定的数据时的响应。 
    unittest 提供一个基类: TestCase ,用于新建测试用例。

3 套件:test suite

    test suite 是一系列的测试用例,或测试套件,或两者皆有。它用于归档需要一起执行的测试。
    诸如: setUp(),tearDown(), setUpClasee(), tearDownClass(). setUpModule(), tearDownModule()

4 执行器:test runner

    test runner 是一个用于执行和输出测试结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。
  • go内置的 testing 包,其测试方式为:

       1  测试用例封装在普通go函数中,
       2 针对给定的输入数据,比较被测试函数,方法返回的实际结果值和预期值,
       若有差异,则通过testing 包提供的函数输出差异信息。
    
       3 失败断定为进入  Error/Errorf(退出当前用例), 
       Fatal/Fatalf(立刻退出测试主进程), 
       当函数进入 这些地方将被记录在testing的输出报告中。
    
       4 go 内置了不少特别的测试方式,如性能基准,输出接口,模糊测试,关键测试等等。
    

    更多信息:

    http://docs.python.org/library/unittest.html

    http://pkg.go.dev

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。