Golang 高质量单测之 Table-Driven:从入门到真香

一个开发人员,在不受外力胁迫的情况下 ,如何能自觉自愿写单测?

那必然是相信收益 > 成本、单测节省的未来修bug时间 > 写单测所花费的时间。

为了保证上述不等式成立,这边建议您考虑 table-driven
方法,快速、无痛写出高质量单测,以降低“我要写单测”这事的心理门槛,最终达到信手拈来、一直写一直爽的神奇效果。

What:什么是 table-driven?


表驱动法(Table-Driven Approach)这个概念,并不是 Golang 或者测试领域独有的;它是个编程模式,属于数据驱动编程的一种。

表驱动法的核心在于:把易变的数据部分,从稳定的处理数据的流程里分离,放进表里;而不是直接混杂在 if-else / switch-case 的多个分支里。

简单举例:写一个 func,输入第 index 天,输出这天是星期几。
假如一周只有两三天,那么直接用 if-else / switch-case,倒也ok。

但如果一周有七天,这代码就有些离谱了:

// GetWeekDay returns the week day name of a week day index.
func GetWeekDay(index int) string {
   
   
   if index == 0 {
   
   
      return "Sunday"
   }
   if index == 1 {
   
   
      return "Monday"
   }
   if index == 2 {
   
   
      return "Tuesday"
   }
   if index == 3 {
   
   
      return "Wednesday"
   }
   if index == 4 {
   
   
      return "Thursday"
   }
   if index == 5 {
   
   
      return "Friday"
   }
   if index == 6 {
   
   
      return "Saturday"
   }
   return "Unknown"
}

显然,控制流程的逻辑并不复杂,是个简单粗暴的映射(0 -> Sunday,1 -> Monday……);分支与分支之间的唯一区别,在于可变的数据,而不是流程本身。

那如果把数据拆分出来,放入表的多个行里(表一般用数组实现;数组的一项即是表的一行),将大量的重复流程消消乐,代码就简洁很多:

// GetWeekDay returns the week day name of a week day index.
func GetWeekDay(index int) string {
   
   
   if index < 0 || index > 6 {
   
   
      return "Unknown"
   }
   weekDays := []string{
   
   "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
   return weekDays[index]
}

把这套方法搬到单测领域,也是如此。

一个测试用例,一般包括以下部分:

  • 稳定的流程
    • 定义测试用例
    • 定义输入数据和期望的输出数据
    • 跑测试用例,拿到实际输出
    • 比较期望输出和实际输出
  • 易变的数据
    • 输入的数据
    • 期望的输出数据

而 table-driven 单测法,就是将流程沉淀为一个可复用的模板、并交由机器自动生成;人类则只需要准备数据部分,将自己的多条不同的数据一行行填充到表里,交给流程模板去构造子测试用例、查表、跑数据、比对结果,写单测这事就大功告成了。

Why:为啥单测要 table-driven?


在了解了 table-driven 的概念后,你多半能预见到,table-driven 单测可带来以下好处:

  • 写得快:人类只需准备数据,无需构造流程。
  • 可读性强:将数据构造成表,结构更清晰,一行一行的数据变化对比分明。
  • 子测试用例互相独立:每条数据是表里的一行,被流程模板构造成一个独立的子测试用例。
  • 可调试性强:因为每行数据被构造成子测试用例,可以单独跑、单独调试。
  • 可扩展/可维护性强:改一个子测试用例,就是改表里的一行数据。

接下来,通过举例对比 TestGetWeekDay 的不同单测风格,就能愈发看出 table-driven 的好处。

例子一:低质量单测之平铺多个 test case

从 0 -> Sunday,1 -> Monday…… 到 6 -> Saturday,给每条数据都写一个单独的 test case:

// test case for index=0
func TestGetWeekDay_Sunday(t *testing.T) {
   
   
   index := 0
   want := "Sunday"
   if got := GetWeekDay(index); got != want {
   
   
      t.Errorf("GetWeekDay() = %v, want %v", got, want)
   }
}

// test case for index=1
func TestGetWeekDay_Monday(t *testing.T) {
   
   
   index := 1
   want := "Monday"
   if got := GetWeekDay(index); got != want {
   
   
      t.Errorf("GetWeekDay() = %v, want %v", got, want)
   }
}

...

一眼望去,重复代码太多,可维护性差;另外,这些针对同一个方法的 test case,被拆成并列的多个,跟其他方法的 test case 放在同一文件里平铺的话,缺乏结构化的组织,可读性差。

例子二:低质量单测之平铺多个 subtest

实际上,从 Go 1.7 开始,一个 test case 里可以有多个子测试(subtest),这些子测试用 t.Run 方法创建:

func TestGetWeekDay(t *testing.T) {
   
   
   // a subtest named "index=0"
   t.Run("index=0", func(t *testing.T) {
   
   
      index := 0
      want := "Sunday"
      if got := GetWeekDay(index); got != want {
   
   
         t.Errorf("GetWeekDay() = %v, want %v", got, want)
      }
   })

   // a subtest named "index=1"
   t.Run("index=1", func(t *testing.T) {
   
   
      index := 1
      want := "Monday"
      if got := GetWeekDay(index); got != want {
   
   
         t.Errorf("GetWeekDay() = %v, want %v"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值