Go语言快排实现TDD实践报告 - Go语言中文社区

Go语言快排实现TDD实践报告


Go语言快排实现TDD实践报告

实验环境

操作系统:Ubuntu18.04.5LTS-amd64
编辑器:VScode

概念解析

TDD(Test-Driven Development)

TDD过程

  • 编写一个失败的测试,并查看失败信息,我们知道现在有一个为需求编写的相关的测试,并且看到它产生了易于理解的失败描述。
  • 编写最少量的代码使其通过,以获得可以运行的程序。
  • 然后不断重构,基于测试的安全性,以确保我们拥有易于使用的精心编写的代码。

TDD功能

  • 便于程序测试,节省调试时间。
  • 增强需求分析,加深对用户需求的理解。
  • 增强开发与测试的协调沟通。
  • 迭代开发,重构后仍可返回上一次能够通过测试的代码。

重构

不改变系统的外部功能,只对内部的结构进行重新的整理。通过重构,不断的调整系统的设计模式和架构,改善其质量、性能,提高其扩展性和维护性,使系统对于需求的变更始终具有较强的适应能力。

TDD与重构有着紧密的联系,在TDD过程中可以看到,代码正是通过不断重构来适应测试,进而满足用户需求的。

单元测试与基准测试

单元测试

单元测试是功能测试,测试各函数的功能是否正常,其还包括覆盖率测试,可视化、量化展示测试的覆盖率(如哪些函数在测试中未涉及等),帮助程序员尽可能测试所有相关的代码。

Go的单元测试框架的要求:

  1. 文件命名规则:
    含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
    单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名。
  2. 函数声明规则:
    测试函数的签名必须接收一个指向testing.T类型的指针,并且函数没有返回值。
  3. 函数命名规则:
    单元测试的函数名必须以Test开头,是可导出公开的函数,最好是Test+要测试的方法函数名。

基准测试

基准测试是测试代码性能的方法,主要通过测试CPU和内存等因素,来评估代码性能,帮助程序员提高代码性能。

Go的基准测试框架的要求:

  1. 文件命名规则:
    含有测试代码的go文件以_test.go结尾,Go语言测试工具只认符合这个规则的文件
    测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名。
  2. 函数声明规则:
    测试函数的签名必须接收一个指向testing.B类型的指针,并且函数没有返回值。
  3. 函数命名规则:
    单元测试的函数名必须以Benchmark开头,是可导出公开的函数,最好是Benchmark+要测试的方法函数名。
  4. 函数体设计规则:
    b.N 是基准测试框架提供,用于控制循环次数,循环调用测试代码评估性能。
    b.ResetTimer()/b.StartTimer()/b.StopTimer()用于控制计时器,准确控制用于性能测试代码的耗时。

“迭代”章节的练习

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

原测试代码部分:

package iteration

import "testing"

func TestRepeat(t *testing.T) {
	repeated := Repeat("a")
	expected := "aaaaa"

	if repeated != expected {
		t.Errorf("repeated '%q' expected '%q'", repeated, expected)
	}
}

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

原代码

package iteration

func Repeat(ch string) string {
	var repeated string
	for i := 0; i < 5; i++ {
		repeated += ch
	}
	return repeated
}
  1. 修改测试代码
package iteration

import "testing"

func TestRepeat(t *testing.T) {
	repeated := Repeat("a", 5)
	expected := "aaaaa"

	if repeated != expected {
		t.Errorf("repeated '%q' expected '%q'", repeated, expected)
	}
}

func BenchmarkRepeat(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Repeat("a", 5)
	}
}
$ go test       //测试出错
# github.com/LEEzanhui/iteration
./iteration_test.go:6:20: too many arguments in call to Repeat
        have (string, number)
        want (string)
FAIL    github.com/LEEzanhui/iteration [build failed]
  1. 修复代码
package iteration

func Repeat(ch string, times int) string {
	var repeated string
	for i := 0; i < times; i++ {
		repeated += ch
	}
	return repeated
}
$ go test       //单元测试成功
PASS
ok      github.com/LEEzanhui/iteration  0.001s
$ go test -bench=.      //基准测试
goos: linux
goarch: amd64
pkg: github.com/LEEzanhui/iteration
BenchmarkRepeat-8       10000000               126 ns/op
PASS
ok      github.com/LEEzanhui/iteration  1.402s

测试成功,代码完成重构;

2. 写一个 ExampleRepeat 来完善你的函数文档

通过查阅文档可知这是一个示例函数,其有格式要求:以Example开头,如果示例函数包含以 “Output” 开头的行注释,在运行测试时,go 会将示例函数的输出和 “Output” 注释中的值做比较;
iteration_test.go文件中,编写一个ExampleRepeat函数,演示对Repeat函数的调用:

func ExampleRepeat() {
	str1 := "a"
	str2 := Repeat(str1, 5)
	fmt.Println(str2)
	// Output: aaaaa
}

可以运行该函数:
7

如果运行结果与注释中的Output不同,会报错:
6

3. 看一下 strings 包。找到你认为可能有用的函数,并对它们编写一些测试。投入时间学习标准库会慢慢得到回报。

访问https://godoc.org/strings可以查看这个包的详细文档,深入了解各个函数。
1

选择了Count、Index和ToLower三个函数;测试了这些函数的普通情况和一些特殊情况,如空串等,具体可见代码;

package teststringpkg

import (
	"strings"
	"testing"
)

func TestCount(t *testing.T) {
	assertCorrectMessage := func(t *testing.T, got, want int) {
		t.Helper()
		if got != want {
			t.Errorf("got '%d' want '%d'", got, want)
		}
	}

	t.Run("count number", func(t *testing.T) {
		got := strings.Count("engineer", "e")
		want := 3

		assertCorrectMessage(t, got, want)
	})

	t.Run("substr is empty", func(t *testing.T) {
		got := strings.Count("engineer", "")
		want := 9

		assertCorrectMessage(t, got, want)
	})
}

func TestIndex(t *testing.T) {
	assertCorrectMessage := func(t *testing.T, got, want int) {
		t.Helper()
		if got != want {
			t.Errorf("got '%d' want '%d'", got, want)
		}
	}

	t.Run("find index", func(t *testing.T) {
		got := strings.Index("chicken", "ck")
		want := 3

		assertCorrectMessage(t, got, want)
	})

	t.Run("substr not present", func(t *testing.T) {
		got := strings.Index("chicken", "chicks")
		want := -1

		assertCorrectMessage(t, got, want)
	})
}

func TestToLower(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("with symbol", func(t *testing.T) {
		got := strings.ToLower("chicken_!?")
		want := "chicken_!?"

		assertCorrectMessage(t, got, want)
	})

	t.Run("with upper case", func(t *testing.T) {
		got := strings.ToLower("GOPher")
		want := "gopher"

		assertCorrectMessage(t, got, want)
	})
}

运行指令:

$ go test github.com/LEEzanhui/teststringpkg -run ^函数名
如:
$ go test github.com/LEEzanhui/teststringpkg -run ^TestCount

运行结果均为:

ok      github.com/LEEzanhui/teststringpkg      0.001s

如下图,是测试Index函数:
2

TDD 应用:快速排序实现

快速排序原理

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

快速排序实现

  1. 创建quicksort.go和quicksort_test.go文件。
  2. 编写quicksort_test.go
    编写时为了方便比较计算结果,准备用ArrayCompare函数来完成比较,并对该函数也写了测试函数;
package quicksort

import "testing"

func TestQuickSort(t *testing.T) {
	cases := []struct {
		in, want []int
	}{
		{[]int{4, 2, 7, 10, 6, 1, 3}, []int{1, 2, 3, 4, 6, 7, 10}},
		{[]int{7, 8, 5, 4, 1, 0, 2, 9, 3, 6}, []int{0, 1, 1, 3, 4, 5, 6, 7, 8, 9}},
	}

	for _, c := range cases {
		got := QuickSort(c.in, len(c.in))
		if !ArrayCompare(got, c.want) {
			t.Errorf("Quicksort(%d) == %d, want %d", c.in, got, c.want)
		}
	}
}

func TestArrayCompare(t *testing.T) {
	cases := []struct {
		in1, in2 []int
		want     bool
	}{
		{[]int{4, 2, 7, 10, 6, 1, 3}, []int{1, 2, 3, 4, 6, 7, 10}, false},
		{[]int{1, 2, 3}, []int{1, 2, 3}, true},
		{[]int{1, 2, 3}, []int{1, 2}, false},
	}

	for _, c := range cases {
		got := ArrayCompare(c.in1, c.in2)
		if got != c.want {
			t.Errorf("ArrayCompare(%d, %d) == %t, want %t", c.in1, c.in2, got, c.want)
		}
	}
}

显然测试函数无法运行,因为QuickSort和ArrayCompare都未定义;

  1. 先使用最少的代码来让失败的测试先跑起来
package quicksort

// QuickSort quicksort
func QuickSort(in []int, length int) []int {
	out := make([]int, length)
	return out
}

// ArrayCompare : retun true if two int array is same
func ArrayCompare(arr1 []int, arr2 []int) bool {
	return true
}

显然无法通过测试:
5

  1. 把代码补充完整,使得它能够通过测试
    过程中涉及一定的重构,就不逐步展示了;
package quicksort

// QuickSort quicksort
func QuickSort(in []int, length int) []int {
	out := make([]int, length)
	for i := 0; i < length; i++ {
		out[i] = in[i]
	}
	QSRecur(&out, 0, len(out)-1)
	return out
}

// QSRecur recursive called by QuickSort
func QSRecur(in *[]int, l, r int) {
	if l < r {
		i, j, baseNum := l, r, (*in)[l]
		for i < j {
			for i < j && (*in)[j] >= baseNum {
				j--
			}
			if i < j {
				(*in)[i] = (*in)[j]
				i++
			}
			for i < j && (*in)[i] < baseNum {
				i++
			}
			if i < j {
				(*in)[j] = (*in)[i]
				j--
			}
		}
		(*in)[i] = baseNum
		QSRecur(in, l, i-1)
		QSRecur(in, i+1, r)
	}
}

// ArrayCompare : retun true if two int array is same
func ArrayCompare(arr1 []int, arr2 []int) bool {
	if len(arr1) != len(arr2) {
		return false
	}
	for i := 0; i < len(arr1); i++ {
		if arr1[i] != arr2[i] {
			return false
		}
	}
	return true
}
  1. 测试
    除直接用指令启动测试外,vscode也提供了按键实现一键测试,一般在测试文件的开头或函数上方;
    选择文件开头的run package tests,会执行该文件下的所有测试函数,并附带部分参数的结果,见下图:
    3
    可见其不但测试了结果,而且还考虑了运行时间timeout并显示了测试覆盖率,上图显示了测试覆盖了100%的语句;

  2. 修改quicksort_test.go,加入基准测试

func BenchmarkQuickSort(b *testing.B) {
	for i := 0; i < b.N; i++ {
		arr := []int{7, 8, 5, 4, 1, 0, 2, 9, 3, 6}
		QuickSort(arr, len(arr))
	}
}

测试指令

$ go test -bench=.

结果:
4

总结

本次实验的主要目标是了解TDD、测试等概念,并进行实践,对于Go语言这门测试驱动开发的语言,该技能是基础;此外还进一步了解了Go的语法知识,如数组和切片等,能够在编程中进行运用。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/nonoli287/article/details/108836064
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢