前言

我们都知道,越早发现问题越早纠正,那么程序的成本将更小。
每个语言都有针对自身代码的单元测验结构,这儿简略做些对比介绍。

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 := 0i < 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) {
    得到 := Abs(-1)
    假如有!= 1 {
        t.Errorf("Abs(-1) = %d; 想要 1", 得到)
    }
}

由于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 内置了不少特别的测验方法,如功能基准,输出接口,含糊测验,关键测验等等。
    

    更多信息:

    docs.python.org/library/uni…

    pkg.go.dev