天天看点

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

目录

  • TDD(Test-Driven Development)
    • 测试代码的编写
    • 代码重构
  • 迭代
    • 基准(benchmark)测试
    • 完成参考资料《迭代》章节的练习
  • TDD 应用:插入排序实现
  • 小结
    • 程序编写中出现的问题
  • 参考资料

TDD(Test-Driven Development)

TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。

原理:在开发功能代码之前,先编写单元测试用例代码,再依据测试代码编写能够通过测试的产品代码。

功能:

  1. 程序测试,基本的功能,无论什么软件都需要测试程序进行程序(逻辑)正确性的调试。提前编写测试代码,可以节省程序调试的时间。
  2. 增强需求分析,提前编写测试框架,相比于先直接编写程序,程序员需要更加清晰地理解用户的需求,这看似增加了工作量,但从一开始就让程序尽可能地贴合用户的需求,比到后期才因为不符合用户预期而进行大刀阔斧地修改程序要好得多。
  3. 增强团队沟通,避免开发人员与测试人员因沟通不足,导致矛盾的产生。
  4. 迭代开发,开发过程中的每个节点的代码都是能够通过某一次测试的代码,在重构或者添加新功能模块之后导致不可挽回的错误,我们可以返回上一次能够通过测试的代码。

测试代码的编写

编写测试和函数很类似,其中有一些规则:

程序需要在一个名为 xxx_test.go 的文件中编写

测试函数的命名必须以单词 Test 开始

测试函数只接受一个参数 t *testing.T

具体的例子可以参考:https://blog.csdn.net/weixin_43536737/article/details/108512121

代码重构

这是一个很宽泛的概念,但基本就是在程序的功能完成之后,我们希望代码能够在系统的功能不发生改变的前提下,尽可能简化。简化的目的可以是为了增强代码的可读性,也可以是在简化的过程中找出一些不易发现的逻辑错误。

下面举几个从参考资料中学到的例子:

  1. 将频繁使用的常量提取出来,使用 const 声明,并放在文件头部。这就像 C 语言中往往将程序中使用的幻数(如:某个变量固定的最大值)定义为宏。
  2. 当某个程序块用到多个 if 的时候,可以用 switch 进行优化。
  3. 当某个函数变得非常臃肿的时候(多个功能集成在一起),可以把某个功能分出去,另定义一个函数。(这样也有利于提高代码的重用性)
  4. 测试代码同样可以重构:
func TestHello(t *testing.T) {
    assertCorrectMessage := func(t *testing.T, got, want string) {        
    	t.Helper()        
    	if got != want {            
    		t.Errorf("got '%q' want '%q'", got, want)        
    	}    
    }
    t.Run("saying hello to people", func(t *testing.T) {
    	got := Hello("Chris")        
    	want := "Hello, Chris"        
    	assertCorrectMessage(t, got, want)    
    })
    t.Run("empty string defaults to 'world'", func(t *testing.T) {        
    	got := Hello("")        
    	want := "Hello, World"        
    	assertCorrectMessage(t, got, want)    
    })
}
           

上述程序,使用两个 t.Run 对函数 Hello 的功能进行测试,测试方法都是拿函数返回值与我们预期的值进行比较,由于在两个 Run 之间只有变量的值不同,其它都是相同的,我们采取辅助函数的方式

辅助函数:在 Test 函数中定义的函数,使用 t.Helper() 在函数内部的第一行声明,表示该函数是辅助函数,从而避免编译器将其当作普通程序代码进行编译

在 t.Run 函数中使用了 := ,该操作符可以拆分为两条指令(将变量的声明与定义组合在一起):

var got string
got = Hello("Chris")
           

迭代

Go 使用 for 来进行循环和迭代,而没有 while、do 等关键字。

首先,新建一个文件夹 iteration,在文件夹下创建 iteration_test,go 编写迭代的测试代码:

package iteration
import "testing"
func TestRepeat(t *testing.T) {  
    repeated := Repeat("a")      
    expected := "aaaaa"
    if repeated != expected {        
  	  t.Errorf("expected '%q' but got '%q'", expected, repeated)    
    }
}
           

我们希望 Repeat 函数将字符串 “a” 重复5次,然后返回。

下面创建 iteration.go 文件,并在该文件中实现 Repeat 函数:

package iteration
func Repeat(str string) string {
    var res string        
    res = ""    
    for i := 0; i < 5; i++ {       
	res += str    
    }    
    return res
 }
           

下面在 iteration 文件夹下打开中断,运行:

go test

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

运行结果:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

基准(benchmark)测试

基准测试程序与测试程序都写在 xxx_test.go 文件中。

其形式与 Test 程序也很类似:

测试函数的命名必须以单词 Benchmark 开始

测试函数只接受一个参数 b *testing.B

基准测试原理,是我们可以使用 b.N 获取一个程序运行的次数,然后在 for 循环中运行程序 b.N 次,基准测试框架会给出程序运行的时间。

package iteration
import "testing"
func TestRepeat(t *testing.T) {  
    repeated := Repeat("a")      
    expected := "aaaaa"
    if repeated != expected {        
     t.Errorf("expected '%q' but got '%q'", expected, repeated)    
    }
}

func BenchmarkRepeat(b *testing.B) {    
    for i := 0; i < b.N; i++ {
	Repeat("a")    
    }
}
           

以上是加上了基准测试的 test.go 文件,使用:

go test -bench=

来启动 benchmark 测试,但是这个只能得到一个粗略的结果:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

在 VSCode 中:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

可以直接点击 Benchmark 函数上方自动生成的 “run benchmark” 开启测试:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

这样得到的结果会详细很多,可以看到 Repeat 函数执行了 1000000 次(也即 b.N = 1000000),每次执行该函数平均花费 1.305 ms,16 Bytes的内存,4 次内存的分配。

完成参考资料《迭代》章节的练习

一、修改测试代码,以便调用者可以指定字符重复的次数,然后修复代码。

  1. 修改 iteration_test.go 中的 TestRepeat 函数
func TestRepeat(t *testing.T) {    
    repeated := Repeat("a",5)    
    expected := "aaaaa"
    if repeated != expected {        
    t.Errorf("expected '%q' but got '%q'", expected, repeated)    
    }
}
           
  1. 然后运行 go test 会报错,因为 Repeat “传入参数过多”,根据错误,编写 iteration.go,我们需要修改 Repeat 函数:
func Repeat(str string,count int) string {    
    var res string    
    res = ""    
    for i := 0; i < count; i++ {       
        res += str    
    }   
    return res
}
           

通过测试!

二、写一个 ExampleRepeat 来完善你的函数文档

个人理解是在 iteration.go 文件中,编写一个 ExampleRepeat 函数,演示对 Repeat 函数的调用:

func ExampleRepeat(){
    str1 := "a"    
    str2 := Repeat(str1,10)    
    fmt.Println(str2)
}
           

然后在一个 main package 下

import "iteration"

,调用 ExampleRepeat 函数,运行结果:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

三、看一下 strings 包,找到你认为可能有用的函数,并对它们编写一些测试。

strings 包的部分函数:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

尝试使用 Compare,Index,Fields:

package main

import (
    "fmt"    
    "strings"
)

func main(){
    str1 := "device"    
    str2 := "devise"    
    str3 := "abc de f"    
    // Compare    
    fmt.Println("Compare str1 and str1: %d",strings.Compare(str1,str1))    
    fmt.Println("Compare str1 and str2: %d",strings.Compare(str1,str2))   
    fmt.Println("Compare str2 and str1: %d",strings.Compare(str2,str1))
    // Index    
    fmt.Println("Index of e: %d",strings.Index(str1,"e"))    
    // Field   
    fmt.Println("Fields of str3: %v",strings.Fields(str3))
}
           

运行结果:

Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

稍作分析:Compare 函数比较两个字符串,相同返回 1,左操作数字典序小返回 -1,反之返回1。Index 函数返回字串第一次出现的 index。Fields 函数将字符串按空格拆分,返回数组。

TDD 应用:插入排序实现

  1. 创建 insertSort 文件夹,在其中创建 insertSort_test.go,insertSort.go 文件。
  2. 编写 insertSort_test,go:
package insertSort
import "testing"
func TestInsertSort(t *testing.T) {
    arr := []int{7, 8, 5, 4, 1, 0, 2, 9, 3, 6}    
    sortedArr := InsertSort(arr, 10)    
    expected := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    if !ArrayCompare(sortedArr, 10, expected, 10) {
        t.Errorf("\ngot: '%v'\nwant: '%v'", sortedArr, expected)
    }
}

func BenchmarkRepeat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        arr := []int{7, 8, 5, 4, 1, 0, 2, 9, 3, 6}        
        InsertSort(arr, 10)   
    }
}
           
  1. 根据测试文件,我们需要实现 InserSort 函数,ArrayCompare函数:
package insertSort
func InsertSort(arr []int, size int) []int {    
    for i := 1; i < size; i++ {   
        for j := i; j > 0 && arr[j] < arr[j-1]; j-- {
            tmp := arr[j]            
            arr[j] = arr[j-1]            
            arr[j-1] = tmp        
        }   
    }   
    return arr
}
func ArrayCompare(arr1 []int, size1 int, arr2 []int, size2 int) bool {
    var size int    
    if size1 < size2 { // if else 花括号不能省      
        size = size1    
    } else {        
        size = size2   // 没有三目运算符
    }    
    for i := 0; i < size; i++ {    
        if arr1[i] != arr2[i] {     
            return false        
        }    
    }    
    return true
}
           
  1. 运行结果:

    Test:

    Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料
    benchmarkTest:
    Go TDD、迭代与算法设计TDD(Test-Driven Development)迭代TDD 应用:插入排序实现小结参考资料

小结

程序编写中出现的问题

  1. 数组的传递

    向函数传递不定长度数组,只能传递切片,切片的定义:

对比数组的定义:

可以得出,“切片”其实就是一个不定长度的数组,但是在向函数传递数组的时候,往往需要创滴这样的不定长数组,从而增强函数的重用性。

注意:Go 向函数传递的数组是复制,而不是指针。

  1. Go 中没有三目运算符
  2. if, else 操作符只有一条语句也不能省略花括号。

参考资料

《Learn Go with tests》

继续阅读