Go——单元测试

测试

  • 结束了核心的协程部分,这部分开始讲go的测试

单元测试

  • 这个我们很熟悉了,常用的是t.Log t.Error,这里有点区别
    package mytest
    
    import "testing"
    import "fmt"
    
    func TestError(t *testing.T) {
      fmt.Println("start test")
      t.Error("fatal error")
      fmt.Println("end test") // end test
    }
    
    func TestFatal(t *testing.T) {
      fmt.Println("start test")
      t.Fatal("fatal error")
      fmt.Println("end test") // 不会执行
    }
    
  • 也可以下载断言测试包
    package mytest
    
    import "testing"
    import "fmt"
    import "github.com/stretchr/testify/assert"
    
    func Square(input int) int {
      return input * input
    }
    
    func TestAssert(t *testing.T) {
      inputs := [...]int{1,2,3}
      expected := [...]int{1,4,9}
      for i:=0; i<len(inputs); i++ {
        ret := Square(inputs[i])
        assert.Equal(t, expected[i], ret)	// 断言相等
      }
    }
    
  • 如果想在命令行中得到测试覆盖率:go test -v -cover 会执行该目录下的全部测试文件并给出coverage
  • 类似的可以使用go build -race 来进行数据竞争检测,看是否加锁

benchmark

  • 一般是看哪个库更好用做一个测评,或者代码怎么写性能更好做测试
  • 和Test类似,以_test.go结尾文件,看个字符串拼接的例子
    package mybench
    
    import "testing"
    import "bytes"
    
    func BenchmarkConcatStringByAdd(b *testing.B) {
      elements := []string{"1", "2", "3", "4"}
      b.ResetTimer()  // 假装运行别的代码呢
      b.Log(b.N)  // 这是个随机值
      for i:=0; i<b.N; i++ {  // 多来几遍
        ret := ""
        for _, e := range elements {
          ret += e
        }
      }
      b.StopTimer()
    }
    
    func BenchmarkConcatStringByBuffer(b *testing.B) {
      elements := []string{"1", "2", "3", "4"}
      b.ResetTimer()  // 假装运行别的代码呢
      for i:=0; i<b.N; i++ {  // 多来几遍
        var buf bytes.Buffer	// 类似 .join() 方法
        for _, e := range elements {
          buf.WriteString(e)
        }
      }
      b.StopTimer()
    }
    
    1
  • 为什么用buffer更快呢?用-benchmem参数便知:
    2
    • buffer只分配了一次内存,而+拼接每拼一个就要分配新空间!之前的泄露
  • 后面分析代码性能的时候会经常用到

BDD

  • behavior driven development
  • 为降低开发人员和验收人员之间的沟通成本,推出了很多BDD的测试框架
  • 例如:goconvey
    // 安装:go get github.com/smartystreets/goconvey/convey
    // git config --global --unset http.proxy
    // git config --global --unset https.proxy
    package mybdd
    
    import "testing"
    import . "github.com/smartystreets/goconvey/convey" // 使用 . 做别名可以直接使用其方法
    
    
    func TestBDD(t *testing.T) {
      Convey("given two even numbers", t, func() {  // 只有最外层的Convey需要传入*testing.T类型的变量t
        a := 2
        b := 4
        Convey("when add two numbers", func() {     // 第三个参数习惯以闭包的形式实现
          c := a+b
          Convey("the result is still even", func() {
            So(c%2, ShouldEqual, 0) // 断言
          })
        })
      })
    }
    
  • 我们看结果:
    === RUN   TestBDD
    
      given two even numbers 
        when add two numbers 
          the result is still even .
    
    
    1 total assertion
    
    --- PASS: TestBDD (0.00s)
    
    • 这个测试框架从输入、到代码大致流程、到最后的结果都呈现出来,对开发人员和客户(测试)都比较友好
    • 还提供了web界面:$GOPATH/bin/goconvey
  • 很多包的功能不能使用的问题
    • 在bin下没有可执行文件
      • Go的包分为两部分,一是GOROOT(安装目录)下自带的包,包括testing等;二是GOPATH(工作目录)下,自己go get的包都会到这
      • 运行时保存可以根据提示go get -u那个包
    • 更新Go版本:直接下载新版本安装覆盖(还是先全部卸载吧)
    • 修改 GO111MODULE 的值的语句是:go env -w GO111MODULE=auto,这个是官方包管理工具,默认不设置
      • off,则到GOPATH(工作目录)定位包
      • on,使用go module管理包,执行文件所在包(文件夹)需要有go.mod文件
      • auto,哪个好用就用哪个

反射

  • 很多编程语言都支持反射
    • 反射出类型:reflect.Type
    • 反射出值:reflect.ValueOf
    package myreflect
    
    import "testing"
    import "reflect"
    import "fmt"
    
    func CheckType(v interface{}) {
      t := reflect.TypeOf(v)  // 类型
      fmt.Println(t)  // .Kind()
      switch t.Kind() { // 判断类型
      case reflect.Float32, reflect.Float64:
        fmt.Println("float")
      case reflect.Int, reflect.Int32, reflect.Int64:
        fmt.Println("int")
      default:
        fmt.Println("啥玩意?",t)
      }
    }
    
    func TestCheckType(t *testing.T) {
      var value float64
      CheckType(&value) // 实参 &
    }
    
    func TestTypeAndValue(t *testing.T) {
      var f int = 10
      t.Log(reflect.TypeOf(f), reflect.ValueOf(f))  // int 10
      t.Log(reflect.ValueOf(f).Type())  // int
    }
    
  • 使用反射可以写更灵活的代码,比如Java的很多框架都是依托反射的原理实现的
    • 反射出类模型,访问属性和方法
      1
    • 看个自定义类的例子:
    package myreflect
    
    import "testing"
    import "reflect"
    
    type Empolyee struct {
      EmpolyeeID string
      Name       string `format:"normal"` // k-v 结构的标记,后面学json解析要用到
      Age        int
    }
    
    func (e *Empolyee) UpdateAge(newAge int) {
      e.Age = newAge
    }
    
    type Customer struct {
      CookieID string
      Name     string
      Age      int
    }
    
    func TestInvokeByName(t *testing.T) { // invoke 调用
      e := &Empolyee{"1", "Roy", 18}	// 赋值给 e
      t.Logf("Name: %[1]v, Type: %[1]T ", reflect.ValueOf(*e).FieldByName("Name"))  // Name: Roy, Type: reflect.Value  %[1] 都用第一个参数
      if name, ok := reflect.TypeOf(*e).FieldByName("Name"); !ok {  // Type和Value都有此方法,注意区别
        t.Error("fail to get 'Name' field")
      }else {
        t.Logf("Name: %v", name)  // Name: {Name  string format:"normal" 16 [1] false}
        t.Log("Tag:", name.Tag.Get("format")) // Tag: normal
      }
      reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(2)})// 得到方法, 并调用;这个传参怎么理解
      t.Log("Update Age:", e) // Update Age: &{1 Roy 1}
    }
    

万能程序

  • 之前说过map只能和nil比较,用逻辑运算符
  • 但是reflect还提供了一种可以比较的方法:DeepEqual()
    package myreflect
    
    import "testing"
    import "reflect"
    
    func TestDeepEqual(t *testing.T) {
      a := map[int]string{1:"one", 2:"two"}
      b := map[int]string{1:"one", 2:"two"}
    
      // t.Log(a==b) // invalid operation: a == b (map can only be compared to nil)
      t.Log(reflect.DeepEqual(a,b)) // true
    
      s1 := []int{1,2,3}
      s2 := []int{1,2,3}
      s3 := []int{3,4,5}
      t.Log("s1==s2?", reflect.DeepEqual(s1,s2))  // true
      t.Log("s1==s3?", reflect.DeepEqual(s1,s3))  // false
    }
    
    
  • 使用反射是追求灵活,为了使程序也能更简便,可以自定义一个万能程序,初始化对象
    package myreflect
    
    import "testing"
    import "reflect"
    import "fmt"
    import "errors"
    
    // 万能程序
    
    func fillBySettings(st interface{}, settings map[string]interface{}) error {
      if reflect.TypeOf(st).Kind() != reflect.Ptr { // 如果传入的不是对象指针
        // 实例化
        if reflect.TypeOf(st).Elem().Kind() != reflect.Struct { // 如果传入的也不是对象
          return errors.New("first param is not pointer, error!")
        }
      }
    
      if settings == nil {
        return errors.New("settings is nil, nothing to do")
      }
    
      var (
        field reflect.StructField // reflect中的预定义类型
        ok    bool
      )
    
      for t,v := range settings { // 用map的元素给属性赋值
        fmt.Println(t)  // Name
        fmt.Println(reflect.ValueOf(st))  // &{  0}
        fmt.Println((reflect.ValueOf(st)).Elem()) // {  0}
        fmt.Println((reflect.ValueOf(st)).Elem().Type())  // myreflect.Empolyee
        fmt.Println((reflect.ValueOf(st)).Elem().Type().FieldByName(t)) // {Name  string format:"normal" 16 [1] false} true
        if field, ok = (reflect.ValueOf(st)).Elem().Type().FieldByName(t); !ok { // 如果找不到和map中键名称相一致的属性名称
          continue
        }
        if field.Type == reflect.TypeOf(v) {  // 有这个属性名,而且属性类型也和map中值的类型相同,可以赋值了
          vstr := reflect.ValueOf(st)
          vstr = vstr.Elem()
          vstr.FieldByName(t).Set(reflect.ValueOf(v)) // 设置成map的value部分,不能直接写v
        }
      }
    
      return nil
    }
    
    // 还是用到之前定义的Employee和Customer struct
    func TestFillSettings(t *testing.T) {
      settings := map[string]interface{}{"Name":"Roy", "Age":18}
      e := Empolyee{}
      if err := fillBySettings(&e, settings); err != nil {  // 传指针
        t.Fatal(err)
      }
      t.Log(e)
    
      c := new(Customer)
      if err := fillBySettings(c, settings); err != nil {   // 传对象
        t.Fatal(err)
      }
      t.Log(*c)  // { Roy 18}
    }
    
    • 上面的程序实现了对空对象属性的赋值操作
    • 反射的缺点在于代码的可读性降低

不安全编程

  • go语言不支持强制类型转换,但也不是绝对不行,在unsafe中提供了方法
    package myunsafe
    
    import "unsafe"
    import "testing"
    
    func TestUnsafe(t *testing.T) {
      i := 10
      t.Log(unsafe.Pointer(&i)) // 0xc0000121a8
      f := *(*float64)(unsafe.Pointer(&i))
      t.Log(unsafe.Pointer(&i)) // 查看内存地址 0xc0000121a8
      t.Log(f)  // 5e-323 完全不是10了
    }
    
    type MyInt int
    
    func TestConvert(t *testing.T) {
      a := []int{1,2,3,4} // slice类型
      t.Log(a)  // [1 2 3 4]
      b := *(*[]MyInt)(unsafe.Pointer(&a))  // 强制转换
      t.Log(b)  // [1 2 3 4]  这个可以,只是起了个别名
    }
    
    • 这很明显是不推荐的
  • 原子操作,保证线程安全的一种方式
    package myunsafe
    
    import "unsafe"
    import "testing"
    import "sync/atomic"
    import "sync"
    import "time"
    import "fmt"
    
    func TestAtomic(t *testing.T) {
      var shareBuffer unsafe.Pointer
      t.Logf("%[1]v, %[1]T", &shareBuffer) // 0xc000006038, *unsafe.Pointer
      writeData := func() { // 闭包
        data := []int{}
        for i:=0; i<100; i++ {
          data = append(data, i)
        }
        // 这个切换必须是线程安全的,这里用原子操作实现
        atomic.StorePointer(&shareBuffer, unsafe.Pointer(&data))  // 切换到共享的buffer,存
      }
      readData := func() {
        data := atomic.LoadPointer(&shareBuffer)  // 切换到共享的buffer,读
        fmt.Println(data, *(*[]int)(data))  // 强转,输出指针和内容
      }
    
      var wg sync.WaitGroup
      writeData() // 先写一个,不然读不出来报错
      for i:=0; i<2; i++ {
        wg.Add(1)
        go func() {
          for i:=0; i<4; i++ {  // 一个协程里多次读写,形成并发测试环境
            writeData()
            time.Sleep(time.Microsecond * 100)
          }
          wg.Done() // -1
        }()
        wg.Add(1)
        go func() {
          for i:=0; i<4; i++ {
            readData()
            time.Sleep(time.Microsecond * 100)
          }
          wg.Done()
        }()
      }
      wg.Wait() 
      readData()  // 读最后那个读到的StorePointer处的数据 0xc000004018
    }
    
    • &取地址符号 , 即取得某个变量的地址 , 如 ; &a
    • * 是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑞士_R

修行不易...

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值