服务计算 TDD实践——实现快速排序算法

1. 概念理解——TDD

1.1 什么是TDD?

TDD 是 Test-Driven Development 的首字母缩写,其中文为 “测试驱动开发”,在百度百科中,TDD 的定义如下:

TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。

1.2 TDD 编程方式

在传统的编程方式中,程序员在需求分析结束后开始编码,直到编写完成之后才开始业务逻辑的测试,这使得程序的调试变的困难,而且通过这种方式编写的程序通常逻辑不够清晰,可读性较差,使得程序的修改和维护变得十分困哪。为了消除传统编程方式带来的弊端,TDD 的编程方式与传统方式有很大不同:

  • 先分解任务,将大问题分解为若干个小问题;
  • 举例,用实例化需求,澄清需求细节;
  • 写测试,只关注需求,程序的输入输出,不关心中间过程;
  • 写实现,不考虑别的需求,用最简单的方式满足当前这个小需求;
  • 重构,使先前编写的代码变得整洁规范;
  • 执行测试,若出现问题则补充测试用例,并修正代码;
  • 代码编写完成。

1.3 TDD 的优点

  • 我们每次只需实现一个子任务,降低了开发者的思维负担;
  • 每次实现一个子任务都需进行单元测试,可以及时修正错误;
  • 提前编写测试可以帮助我们明确需求及其细节,避免需求理解的偏差等问题;
  • 覆盖完全的单元测试使得优化代码或修改业务逻辑时变得安全;

1.4 TDD 的流程

  • 写一个测试用例;
  • 尝试运行测试(必然失败);
  • 先用最少的代码通过编译,执行测试;
  • 把代码补充完整,使得它能通过测试;
  • 代码重构,提高代码质量;
  • 编写、执行基准测试。

1.5 相关词汇理解

  • 重构:改善代码的内部结构,提升代码的质量和性能;
  • 测试:功能测试,用于测试程序的逻辑是否正确;
  • 基准测试:性能测试,用于测试函数性能。

2. “迭代”章节练习

练习:《Learn Go with tests》——迭代

2.1 修改测试代码,以指定字符重复的次数,然后修复代码

修改测试代码如下:

package iteration

import (
    "testing"
	"os"
    "strconv"
)

func TestRepeat(t *testing.T) {
	repeatCount,_ := strconv.Atoi(os.Args[len(os.Args)-1])
	repeated := Repeat("a", repeatCount,_ := strconv.Atoi(os.Args[len(os.Args)-1]))
    
	var expected string
	for i:=0; i<repeatCount; i++{
		expected += "a"
	}

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

此时使用测试命令 go test ./iteration -args repeatCount 进行测试,其中参数 repeatCount 指定了字符重复次数。测试结果如下,此时因为尚未修改 repeat 函数,报错信息提示输入参数过多:

带参数测试

之后修复 repeat 函数:

package iteration

func Repeat(character string, repeatCount int) string {
    var repeated string
    for i := 0; i < repeatCount; i++ {
        repeated += character
    }
    return repeated
}

再次执行测试,能够顺利通过:
测试通过

2.2 写一个 ExampleRepeat 完善函数文档

repeat_test.go 文件中增加以下示例测试函数:

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

因为在该测试函数中使用了 fmt.Println 函数,因此在测试文件中需要额外导入 fmt 包。
另外需要注意的是,测试函数中的测试语句 //Output: aaaaa 是不可省略的,注释中指定了示例测试的输出,若示例函数的输出结果与注释中的结果不同,则测试失败。

2.3 编写测试函数,测试 string 包中的函数

选择 strings.compare 函数进行测试,需要编写测试文件 strings_test.go ,如下:

package strings

import (
    "fmt"
    "testing"
)

func TestCompare(t *testing.T) {
    got1 := strings.Compare("a", "b")
    expected1 := -1
    if got1 != expected1 {
        t.Errorf("expected '%v' but got '%v'", expected1 , got1)
    }

    got2 := strings.Compare("a", "a")
    expected2 := 0
    if got2 != expected2 {
        t.Errorf("expected '%v' but got '%v'", expected2, got2)
    }
    
	got3 := strings.Compare("b", "a")
    expected3 := 1
    if got3 != expected3 {
        t.Errorf("expected '%v' but got '%v'", expected3, got3)
    }
}

func ExampleCompare() {
    str := strings.Compare("abc", "abc")
    fmt.Println(str)
    //Output: 0

}

使用 go test ./strings_test.go 命令进行测试,可以通过:
go test通过

3. TDD实现快速排序

3.1 编写测试用例 quicksort_test.go

单元测试的编写格式为:

  • 文件名必须以 _test.go 结尾;
  • 方法名必须是 Test 开头;
  • 方法参数必须是 t *testing.T

因为我们需要实现的功能为快速排序,因此我们需要测试一个无序数组在经过 Quicksort 函数处理后是否有序(升序),据此可以编写如下测试:

package sort

import "testing"

func TestQuicksort(t *testing.T){
	cases := []struct {
		in, want []int
	}{
		{ []int{5, 3, 2, 1, 4}, []int{1, 2, 3, 4, 5} },
	}

	for _, c := range cases {
		temp := c.in
		Quicksort(c.in)
		for i := 0; i < len(c.in); i++ {
			if c.in[i] != c.want[i] {
				t.Errorf("Quicksort(%q) == %q, want %q", temp, c.in, c.want)
				break
			}
		}
	}
}

3.2 尝试运行测试

使用 go test ./sort 尝试运行测试,显然,因为我们尚未实现 Quicksort 函数,因此该测试必定是失败的。虽然我们知道该测试不可能成功,但我们还是必须进行此次尝试,使得运行结果使我们所期待的失败(找不到 Quicksort 函数),而不是其他失败(编译错误等),这样当程序出现错误的时候,我们可以很快定位到出错的位置。运行测试的结果如下:
go test 无法执行

3.3 用最少的代码通过编译,执行测试

为了使测试过程不再出现编译错误,我们需要先定义 Quicksort 函数供测试函数调用:

package sort

func Quicksort(s []int) []int {
	return s
}

此时执行测试的结果如下:

go test 成功执行

3.4 把代码补充完整,使得它能通过测试

package sort

func Quicksort(s []int) {
	if len(s) > 0 {
		x := s[0]
		i := 0
		j := len(s) - 1
		
		for i < j {
			for ; i < j; j-- {
				if s[j] < x {
					s[i] = s[j]
					i++
					break
				}
			}
			
			for ; i < j; i++ {
				if s[i] > x {
					s[j] = s[i]
					j--
					break
				}
			}	
		}
	
		s[i] = x
		
		Quicksort(s[:i])
		Quicksort(s[i+1:])
	}
}

执行测试,如果测试失败,根据失败信息可以快速定位到出错位置进行更正,直到通过测试:

go test 测试通过

3.5 代码重构,提高代码质量

对过长的快速排序函数进行重构,将其分解为较短的,易理解的,重用性较高的几个函数:

package sort

func AdjustNum(s []int, x int, i int, j int) (int, int) {
	for ; i < j; j-- {
		if s[j] < x {
			s[i] = s[j]
			i++
			break
		}
	}
	
	for ; i < j; i++ {
		if s[i] > x {
			s[j] = s[i]
			j--
			break
		}
	}
	return i, j
}

func AdjustSlice(s []int) int {
	x := s[0]
	i := 0
	j := len(s) - 1
		
	for i < j {
		i, j = AdjustNum(s, x, i, j)
	}
	s[i] = x
	
	return i	
}

func Quicksort(s []int) {
	if len(s) > 0 {
		i := AdjustSlice(s)
		Quicksort(s[:i])
		Quicksort(s[i+1:])
	}
}

再次执行测试,确保快速排序函数的功能不发生改变:

go test 测试通过

3.6 编写、执行基准测试

基准测试的编写格式为:

  • 文件名必须以 _test.go 结尾;
  • 方法名必须是 Test 开头;
  • 方法参数必须是 b *testing.B

按照上面的要求编写基准测试(将基准测试与单元测试放在同一测试文件中):

func BenchmarkQuicksort(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Quicksort([]int{5, 3, 4, 2, 1})
    }
}

基准测试的结果为:

基准测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值