golang学习随便记15

golang测试

go test 命令

go test 命令是一个按照一定的约定和组织来测试代码的程序。我们需要了解有哪些约定和组织:在包目录内,所有后缀为 _test.go 的源文件不会被 go build 构建命令构建成包的一部分,相反,它们会被 go test 命令使用

在 *_test.go 文件中,有3种类型的函数:测试函数、基准(benchmark)函数、示例函数。

一个测试函数Test为函数名的前缀,用来测试程序的一些逻辑行为是否正确;go test 命令会调用这些测试函数并报告测试结果是 PASS 或 FAIL。

基准函数则以 Benchmark 为函数名前缀,是用来衡量一些函数的性能的;go test 命令会多次运行基准测试函数以计算一个平均的执行时间。

示例函数是以 Example 为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。

go test 命令会遍历所有的 *_test.go 文件中符合上述命名规则的函数,生成一个临时的 main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。

测试函数

要使用测试函数,必须导入 testing 包 (一般第一个参数 t 为 *testing.T 类型,需要用来报告测试情况),而且测试函数的名字除了必须以 Test 开头,可选的后缀名必须以大写字母开头

书上给出的例子是判断回文字符串,功能函数 IsPalindrome 和测试函数 TestPalindrome、TestNonPalindrome 都属于 package word,不过前者和后者是不同的文件中的,因为前者是 go build 用的,而后者是 go test 用的。

word.go 源码文件如下

package word

func IsPalindrome(s string) bool {
	for i := range s {
		if s[i] != s[len(s)-1-i] {
			return false
		}
	}
	return true
}

测试文件 word_test.go 如下

package word

import "testing"

func TestPalindrome(t *testing.T) {
	if !IsPalindrome("detartrated") {
		t.Error(`IsPalindrome("detartrated") = false`)
	}
	if !IsPalindrome("kayak") {
		t.Error(`IsPalindrome("kayak") = false`)
	}
}

func TestNonPalindrome(t *testing.T) {
	if IsPalindrome("palindrome") {
		t.Error(`IsPalindrome("palindrome") = true`)
	}
}

在项目目录下,运行 go  test ,可以看到测试文件(这里只有1个)测试结果 PASS 或 FAIL,以及总的项目测试情况 ok 或 FAIL,项目路径和测试耗时

PS C:\Users\zime\go\src\word> go test
PASS
ok      word    0.030s

很好,两个测试用例都过了。结果一个中国人发现这个函数不能把“山上人人上山”这样的汉语识别为回文,而一个美国中部用户抱怨中间有标点空格和大小写的“A man, a plan, a canal: Panama.” 也不能识别为回文。遇到用户的错误报告,良好的习惯是编写测试用例来复现用户观察到的结果,这样能更好定位要解决的问题,也能知道解决了没有。我们添加两个测试函数:TestChinesePalindrome 和 TestCanalPalindrome,而且对于长句子,我们把它放入变量 input,避免两次输入长句子。

func TestChinesePalindrome(t *testing.T) {
	if !IsPalindrome("山上人人上山") {
		t.Error(`IsPalindrome("山上人人上山") = false`)
	}
}

func TestCanalPalindrome(t *testing.T) {
	input := "A man, a plan, a canal: Panama"
	if !IsPalindrome(input) {
		t.Errorf(`IsPalindrome(%q) = false`, input)
	}
}

再次运行测试

PS C:\Users\zime\go\src\word> go test
--- FAIL: TestChinesePalindrome (0.00s)
    word_test.go:22: IsPalindrome("山上人人上山") = false
--- FAIL: TestCanalPalindrome (0.00s)
    word_test.go:29: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL    word    0.046s

很好,我们复现了客户的抱怨情况。先写测试用例,然后不断完善代码,可以让我们进行快速迭代,即所谓 TDD。不过这里有个问题,如果测试集有很多运行缓慢的测试,而我们只是修改了局部,反复运行测试非常耗时间,怎样避免运行所有测试函数呢?

先用 go  test  -v 打印出每个测试函数的名字和运行时间

PS C:\Users\zime\go\src\word> go test -v
=== RUN   TestPalindrome
--- PASS: TestPalindrome (0.00s)
=== RUN   TestNonPalindrome
--- PASS: TestNonPalindrome (0.00s)
=== RUN   TestChinesePalindrome
    word_test.go:22: IsPalindrome("山上人人上山") = false
--- FAIL: TestChinesePalindrome (0.00s)
=== RUN   TestCanalPalindrome
    word_test.go:29: IsPalindrome("A man, a plan, a canal: Panama") = false
--- FAIL: TestCanalPalindrome (0.00s)
FAIL
exit status 1
FAIL    word    0.049s

然后用 -run 参数指定一个正则表达式来筛选需要测试的函数,即 go  test  -v  -run="<regexp>"

C:\Users\zime\go\src\word>go test -v -run="Chinese|Canal"
=== RUN   TestChinesePalindrome
    word_test.go:22: IsPalindrome("山上人人上山") = false
--- FAIL: TestChinesePalindrome (0.00s)
=== RUN   TestCanalPalindrome
    word_test.go:29: IsPalindrome("A man, a plan, a canal: Panama") = false
--- FAIL: TestCanalPalindrome (0.00s)
FAIL
exit status 1
FAIL    word    0.106s

这里有个小坑,带 -run 参数,不能在 windows powershell 环境下,切换到 cmd 才行。)这样,我们就只运行了2个测试用例,可以节约时间。当然,在修复 bug 后,需要运行一遍全部的测试用例,确保不会在修复 bug 中引入了新的问题。

通过分析,我们发现,第一个bug是因为我们对字符串的处理,是基于 byte,而非码点 rune序列,这样,对于非 ASCII 字符就会有问题。第二个bug则是因为我们没有忽略空格和字母的大小写等导致的。我们改成可以识别汉字回文、字母回文(忽略大小写),改写 IsPalindrome函数。

func IsPalindrome(s string) bool {
	var letters []rune
	for _, r := range s {
		if unicode.IsLetter(r) {
			letters = append(letters, unicode.ToLower(r))
		} else if unicode.Is(unicode.Han, r) {
			letters = append(letters, r)
		}
	}
	for i := range letters {
		if letters[i] != letters[len(letters)-1-i] {
			return false
		}
	}
	return true
}

再次运行

C:\Users\zime\go\src\word>go test -v -run="Chinese|Canal"
=== RUN   TestChinesePalindrome
--- PASS: TestChinesePalindrome (0.00s)
=== RUN   TestCanalPalindrome
--- PASS: TestCanalPalindrome (0.00s)
PASS
ok      word    0.043s

与此同时,我们知道之前我们的测试用例太少,不能很好发现问题,而且各种情况都写一个函数非常麻烦,所以,把测试数据合并成表格式的数据,然后循环中测试(只需要保留一个测试函数 TestIsPalindrome 即可,添加测试数据非常方便)

package word

import "testing"

func TestIsPalindrome(t *testing.T) {
	var tests = []struct {
		input string
		want  bool
	}{
		{"", true},
		{"a", true},
		{"aa", true},
		{"ab", false},
		{"kayak", true},
		{"detartrated", true},
		{"A man, a plan, a canal: Panama", true},
		{"山上人人上山", true},
		{"Evil I did dwell; lewd did I live.", true},
		{"Able was I ere I saw Elba", true},
		{"été", true},
		{"Et se resservir, ivresse reste.", true},
		{"palindrome", false}, // non-palindrome
		{"desserts", false},   // semi-palindrome
	}
	for _, test := range tests {
		if got := IsPalindrome(test.input); got != test.want {
			t.Errorf(`IsPalindrome(%q) = %v`, test.input, got)
		}
	}
}

这样,再运行  go  test  发现全部通过即可。

在前面的测试中,一个测试数据失败,并不会停止后续测试进行,这通常符合预期。但对于某种场合,或许一个测试失败,再去测试后面的函数是无意义的(例如前者失败就会导致后者失败),这时,我们可以用 t.Fatal 和 t.Fatalf 代替 t.Error 和 t.Errorf,这样,一旦失败了就会停止测试。不过这里有一个前提,就是必须在同一个 goroutine 内部

测试失败的信息一般形式是“f(x)=y, want z”,其中 f(x)对应失败的操作和对应的输入,y是实际运行结果,z是期望的正确结果(对于返回布尔型结果的函数,如果函数名反映了正确结果应该是什么,那么z不是必需的)。

随机测试

表格驱动的测试便于构造精心挑选的测试用例,例如考虑各种边界和极限情况。另一种测试思路是随机测试,也就是构造广泛的随机输入来测试考察函数的行为。

对于一个随机的输入,我们如何知道希望的输出结果呢?这有两种策略:1、编写对照函数,使用简单和清晰的算法,虽然效率低,但行为要和测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。其实这种手段oier很熟悉,就是死算的算法来检验那个高性能算法的正确性。2、生成的随机输入的数据遵循特定的模式,从而我们知道期望的输出的模式。这种手段其实oier也会采用,其实此时的随机是限定模式的随机。

下面的例子使用了第二种策略。

package word

import (
	"math/rand"
	"testing"
	"time"
)

func TestIsPalindrome(t *testing.T) {
// ....
}

func randomPalindrome(rng *rand.Rand) string {
	n := rng.Intn(25) // random length up to 24
	runes := make([]rune, n)
	for i := 0; i < (n+1)/2; i++ {
		r := rune(rng.Intn(0x1000)) // random rune up to '\u0999'
		runes[i] = r
		runes[n-1-i] = r
	}
	return string(runes)
}

func TestRandomPalindromes(t *testing.T) {
	// Initialize a pseudo-random number generator.
	seed := time.Now().UTC().UnixNano()
	t.Logf("Random seed: %d", seed)
	rng := rand.New(rand.NewSource(seed))

	for i := 0; i < 1000; i++ {
		p := randomPalindrome(rng)
		if !IsPalindrome(p) {
			t.Errorf("IsPalindrome(%q) = false", p)
		}
	}
}

上面的文件word2_test.go测试情况类似如下

sjg@sjg-PC:~/go/src/word2$ go test . -v
=== RUN   TestIsPalindrome
--- PASS: TestIsPalindrome (0.00s)
=== RUN   TestRandomPalindromes
    word2_test.go:49: Random seed: 1710318896279145518
--- PASS: TestRandomPalindromes (0.00s)
PASS
ok      word2   0.003s

测试一个命令

前面的例子都是测试包中的函数,其实 go test 也可以用来测试可执行程序(package main)。

echo.go 生成命令的代码如下

package main

import (
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
)

var (
	n = flag.Bool("n", false, "omit trailing newline")
	s = flag.String("s", " ", "separator")
)

var out io.Writer = os.Stdout // modified during testing

func main() {
	flag.Parse()
	if err := echo(!*n, *s, flag.Args()); err != nil {
		fmt.Fprintf(os.Stderr, "echo: %v\n", err)
		os.Exit(1)
	}
}

func echo(newline bool, sep string, args []string) error {
	fmt.Fprint(out, strings.Join(args, sep))
	if newline {
		fmt.Fprintln(out)
	}
	return nil
}

测试文件 echo_test.go 如下:

package main

import (
	"bytes"
	"fmt"
	"testing"
)

func TestEcho(t *testing.T) {
	var tests = []struct {
		newline bool
		sep     string
		args    []string
		want    string
	}{
		{true, "", []string{}, "\n"},
		{false, "", []string{}, ""},
		{true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},
		{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
		{false, ":", []string{"1", "2", "3"}, "1:2:3"},
		{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
	}
	for _, test := range tests {
		descr := fmt.Sprintf("echo(%v, %q, %q)",
			test.newline, test.sep, test.args)

		out = new(bytes.Buffer) // captured output
		if err := echo(test.newline, test.sep, test.args); err != nil {
			t.Errorf("%s failed: %v", descr, err)
			continue
		}
		got := out.(*bytes.Buffer).String()
		if got != test.want {
			t.Errorf("%s = %q, want %q", descr, got, test.want)
		}
	}
}

测试如下:(下面结果不通过是因为有一行故意错误设置的测试数据,去掉该行就通过了)

sjg@sjg-PC:~/go/src/echo$ go test .
--- FAIL: TestEcho (0.00s)
    echo _test.go:34: echo(true, ",", ["a" "b" "c"]) = "a,b,c\n", want "a b c\n"
FAIL
FAIL    echo    0.001s
FAIL

白盒测试

前面的随机测试,是一种黑盒测试,被测试函数是导出的函数。测试命令中的的例子是白盒测试,因为 echo函数是未导出的内部函数。这个白盒测试中,输出 out 在前一个测试用例完成后是生成新的对象。在白盒测试时,有时需要容易测试的伪对象,方便配置,结果容易预测,更可靠,适合观察,例如涉及数据库更新、发送邮件、信用卡消费行为等场合。

下面的代码演示为用户提供网络存储的web服务当中的配额检查:当用户使用了超过90%的存储配额之后将发送提醒邮件。

package storage

import (
	"fmt"
	"log"
	"net/smtp"
)

func bytesInUse(username string) int64 { return 0 /* ... */ }

// Email sender configuration.
// NOTE: never put passwords in source code!
const sender = "notifications@example.com"
const password = "correcthorsebatterystaple"
const hostname = "smtp.example.com"

const template = `警告:您已使用 %d 字节,占您存储配额的 %d%% 。`

func CheckQuota(username string) {
	used := bytesInUse(username)
	const quota = 1000000000 // 1GB
	percent := 100 * used / quota
	if percent < 90 {
		return // OK
	}
	msg := fmt.Sprintf(template, used, percent)
	auth := smtp.PlainAuth("", sender, password, hostname)
	err := smtp.SendMail(hostname+":587", auth, sender,
		[]string{username}, []byte(msg))
	if err != nil {
		log.Printf("smtp.SendMail(%s) failed: %s", username, err)
	}
}

在测试这个代码时,我们通常不会希望发送真实的邮件(功能测试时可能会,此处是单元白盒测试),为了便于白盒测试,我们重构一下,将邮件处理逻辑放到一个私有的 notifyUser 函数中:

const template = `警告:您已使用 %d 字节,占您存储配额的 %d%% 。`

var notifyUser = func(username, msg string) {
	auth := smtp.PlainAuth("", sender, password, hostname)
	err := smtp.SendMail(hostname+":587", auth, sender,
		[]string{username}, []byte(msg))
	if err != nil {
		log.Printf("smtp.SendMail(%s) failed: %s", username, err)
	}
}

func CheckQuota(username string) {
	used := bytesInUse(username)
	const quota = 1000000000 // 1GB
	percent := 100 * used / quota
	if percent < 90 {
		return // OK
	}
	msg := fmt.Sprintf(template, used, percent)
	notifyUser(username, msg)
}

上面代码的重点是notifyUser函数不是常见的函数定义方式,而是“函数指针”形式,这样它就是一个包内共享的变量(“包范围全局变量”),其他地方可以覆盖它的值让“函数指针”指向其他执行代码。现在就可以在测试中用伪邮件发送函数代替真实的邮件发送函数了:

package storage

import (
	"strings"
	"testing"
)

func TestCheckQuotaNotifiesUser(t *testing.T) {
	var notifiedUser, notifiedMsg string
	notifyUser = func(user, msg string) { // 注意:“函数指针”值覆盖
		notifiedUser, notifiedMsg = user, msg
	}

	// ...simulate a 980MB-used condition...

	const user = "joe@example.org"
	CheckQuota(user)
	if notifiedUser == "" && notifiedMsg == "" {
		t.Fatalf("notifyUser not called")
	}
	if notifiedUser != user {
		t.Errorf("wrong user (%s) notified, want %s",
			notifiedUser, user)
	}
	const wantSubstring = "98% of your quota"
	if !strings.Contains(notifiedMsg, wantSubstring) {
		t.Errorf("unexpected notification message <<%s>>, "+
			"want substring %q", notifiedMsg, wantSubstring)
	}
}

替换邮件发送函数的伪发送函数仅仅是记录了一下谁发送的,发送了什么信息。测试中可以打印这些信息。

上面的“函数指针”替换能帮助我们绕过麻烦的邮件发送过程,但这个替换也存在一定问题:当测试函数返回后, CheckQuota将不能正常工作,因为 notifyUsers 使用的还是测试函数中的伪函数。为了杜绝这个问题,最好的做法是测试完了恢复“函数指针”,使用golang的defer执行来恢复是一个好办法

func TestCheckQuotaNotifiesUser(t *testing.T) {
	savedUser := notifyUser
	defer func() {
		notifyUser = savedUser
	}()

	var notifiedUser, notifiedMsg string
	notifyUser = func(user, msg string) { // 注意:“函数指针”值覆盖
		notifiedUser, notifiedMsg = user, msg
	}

	// 其余代码
}

上面这种全局变量暂存,被修改,defer恢复的模式是安全的,因为go test命令不会同时并发地执行多个测试

外部测试包

上面的例子,我们功能部分storage.go文件隶属于package  storage,而测试代码storage_test.go文件也隶属于同一个package storage。这样做多数时候没有问题,但我们更常见的情况是测试代码隶属于package xxx_test,这是为什么?

在 golang 中有这样两个包:net/url 和 net/http。如果net/url包中存在一个测试,用来演示不同URL和HTTP客户端的交互行为,这时就会存在问题:本身上层的 net/http包依赖下层的 net/url包,而现在测试代码中要求下层的 net/url 使用到上层的 net/http包,就产生了循环依赖问题,而golang是禁止循环依赖的。

解决这个问题的办法就是package  xxx_test形式的外部测试包:让测试代码在 net/url_test 包中(它和 net/url包在同一个目录也是可以的,而且一般也放在同一目录),然后 net/url_test包同时依赖(引用)net/http 和 net/url,这样就解除了循环依赖。在集成测试中,更可能遇到需要外部测试包的情况。

要了解目录下哪些golang源文件是产品代码,哪些是包内测试,哪些是外部测试包,可以用go list命令。例如, go  list  -f={{.GoFiles}}  包名  就可以查看产品代码对应的源文件列表,也就是 go build时需要编译的部分。

外部测试包提高了灵活性(它是独立的,可以“随便import”),但它的外部性也会存在一个问题:外部测试包的确需要访问被测试包内部的代码(即未导出的函数、变量),但测试函数已经和它们不在同一个包下了,失去了访问权限。解决这个问题需要一点点技巧:在包内加一个名称类似 export_test.go 的文件,在该文件中导出一个内部的实现给外部测试包。例如,package fmt内部是 isSpace 函数,在 export_test.go 文件中放入如下代码给外部测试留个后门:

package fmt

var IsSpace = isSpace

测试覆盖率

这里先来关注“接口”一章中的表达式求值例子。先创建接口 Expr 表示 golang 中的任意表达式(暂时它没有任何方法,后面再根据表达式的公共特点来定义接口应该具备的方法)

package eval

type Expr interface{}

type Var string

type literal float64

type unary struct {
	op rune // one of '+', '-'
	x  Expr
}

type binary struct {
	op   rune // one of '+', '-', '*', '/'
	x, y Expr
}

type call struct {
	fn   string // one of "pow", "sin", "sqrt"
	args []Expr
}

我们这里为了演示,作了简化:“表达式语言”包括浮点数(即小数点符号表示的字面值);二元操作符+、-、*、/;表示正负的一元操作符-、+;调用函数,如 pow(x,y)、sin(x)、sqrt(x);变量,如x、pi;当然也有括号和标准的优先级运算符。所有的值都是 float64类型。上面的 ast.go 文件包括了有关类型。为了计算包含变量的表达式,我们需要将变量的名字(字符串)映射成对应的值(float64),在 eval.go文件中包含如下代码定义环境变量类型Env:

type Env map[Var]float64

不管什么样的表达式,都要能利用 Env类型的映射表来计算值,所以,Expr接口中添加 Eval方法(包 eval 只导出 Expr、Env、Var类型,调用方不需要使用具体的表达式类型):

type Expr interface {
	Eval(env Env) float64
}

我们来看具体的表达式类型如何计算值,即它们实现接口方法的情况:

type Env map[Var]float64

func (v Var) Eval(env Env) float64 {
	return env[v]
}

func (l literal) Eval(_ Env) float64 {
	return float64(l)
}

func (u unary) Eval(env Env) float64 {
	switch u.op {
	case '+':
		return +u.x.Eval(env)
	case '-':
		return -u.x.Eval(env)
	}
	panic(fmt.Sprintf("unsupported unary operator: %q", u.op))
}

func (b binary) Eval(env Env) float64 {
	switch b.op {
	case '+':
		return b.x.Eval(env) + b.y.Eval(env)
	case '-':
		return b.x.Eval(env) - b.y.Eval(env)
	case '*':
		return b.x.Eval(env) * b.y.Eval(env)
	case '/':
		return b.x.Eval(env) / b.y.Eval(env)
	}
	panic(fmt.Sprintf("unsupported binary operator: %q", b.op))
}

func (c call) Eval(env Env) float64 {
	switch c.fn {
	case "pow":
		return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
	case "sin":
		return math.Sin(c.args[0].Eval(env))
	case "sqrt":
		return math.Sqrt(c.args[0].Eval(env))
	}
	panic(fmt.Sprintf("unsupported function call: %s", c.fn))
}

要进行具体的表达式计算,有一步工作是把表达式解析切割成一个个token,分类到上面的各种求值类型中。下面的 parse.go 做的是这个工作(非常类似前面的S表达式解码工作),编译器相关的东西都比较复杂,下面仅列出代码:

package eval

import (
	"fmt"
	"strconv"
	"strings"
	"text/scanner"
)

type lexer struct {
	scan  scanner.Scanner
	token rune // current lookahead token
}

func (lex *lexer) next()        { lex.token = lex.scan.Scan() }
func (lex *lexer) text() string { return lex.scan.TokenText() }

type lexPanic string

func (lex *lexer) describe() string {
	switch lex.token {
	case scanner.EOF:
		return "end of file"
	case scanner.Ident:
		return fmt.Sprintf("identifier %s", lex.text())
	case scanner.Int, scanner.Float:
		return fmt.Sprintf("number %s", lex.text())
	}
	return fmt.Sprintf("%q", rune(lex.token)) // any other rune
}

func precedence(op rune) int {
	switch op {
	case '*', '/':
		return 2
	case '+', '-':
		return 1
	}
	return 0
}

func Parse(input string) (_ Expr, err error) {
	defer func() {
		switch x := recover().(type) {
		case nil:
			// no panic
		case lexPanic:
			err = fmt.Errorf("%s", x)
		default:
			// unexpected panic: resume state of panic.
			panic(x)
		}
	}()
	lex := new(lexer)
	lex.scan.Init(strings.NewReader(input))
	lex.scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats
	lex.next() // initial lookahead
	e := parseExpr(lex)
	if lex.token != scanner.EOF {
		return nil, fmt.Errorf("unexpected %s", lex.describe())
	}
	return e, nil
}

func parseExpr(lex *lexer) Expr { return parseBinary(lex, 1) }

func parseBinary(lex *lexer, prec1 int) Expr {
	lhs := parseUnary(lex)
	for prec := precedence(lex.token); prec >= prec1; prec-- {
		for precedence(lex.token) == prec {
			op := lex.token
			lex.next() // consume operator
			rhs := parseBinary(lex, prec+1)
			lhs = binary{op, lhs, rhs}
		}
	}
	return lhs
}

func parseUnary(lex *lexer) Expr {
	if lex.token == '+' || lex.token == '-' {
		op := lex.token
		lex.next() // consume '+' or '-'
		return unary{op, parseUnary(lex)}
	}
	return parsePrimary(lex)
}

func parsePrimary(lex *lexer) Expr {
	switch lex.token {
	case scanner.Ident:
		id := lex.text()
		lex.next() // consume Ident
		if lex.token != '(' {
			return Var(id)
		}
		lex.next() // consume '('
		var args []Expr
		if lex.token != ')' {
			for {
				args = append(args, parseExpr(lex))
				if lex.token != ',' {
					break
				}
				lex.next() // consume ','
			}
			if lex.token != ')' {
				msg := fmt.Sprintf("got %s, want ')'", lex.describe())
				panic(lexPanic(msg))
			}
		}
		lex.next() // consume ')'
		return call{id, args}

	case scanner.Int, scanner.Float:
		f, err := strconv.ParseFloat(lex.text(), 64)
		if err != nil {
			panic(lexPanic(err.Error()))
		}
		lex.next() // consume number
		return literal(f)

	case '(':
		lex.next() // consume '('
		e := parseExpr(lex)
		if lex.token != ')' {
			msg := fmt.Sprintf("got %s, want ')'", lex.describe())
			panic(lexPanic(msg))
		}
		lex.next() // consume ')'
		return e
	}
	msg := fmt.Sprintf("unexpected %s", lex.describe())
	panic(lexPanic(msg))
}

我们来测试一下(eval_test.go文件):

package eval

import (
	"fmt"
	"math"
	"testing"
)

func TestEval(t *testing.T) {
	tests := []struct {
		expr string
		env  Env
		want string
	}{
		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
		{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"},
		{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
		{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
		{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
		{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
	}
	var prevExpr string
	for _, test := range tests {
		// Print expr only when it changes.
		if test.expr != prevExpr {
			fmt.Printf("\n%s\n", test.expr)
			prevExpr = test.expr
		}
		expr, err := Parse(test.expr)
		if err != nil {
			t.Error(err) // parse error
			continue
		}
		got := fmt.Sprintf("%.6g", expr.Eval(test.env))
		fmt.Printf("\t%v => %s\n", test.env, got)
		if got != test.want {
			t.Errorf("%s.Eval() in %v = %q, want %q\n",
				test.expr, test.env, got, test.want)
		}
	}
}

测试结果:

zime@zime-virtual-machine:~/go/src/ch7eval$ go test -v .
=== RUN   TestEval

sqrt(A / pi)
	map[A:87616 pi:3.141592653589793] => 167

pow(x, 3) + pow(y, 3)
	map[x:12 y:1] => 1729
	map[x:9 y:10] => 1729

5 / 9 * (F - 32)
	map[F:-40] => -40
	map[F:32] => 0
	map[F:212] => 100
--- PASS: TestEval (0.00s)
PASS
ok  	ch7eval	0.004s

上面的测试中给出的都是合法的表达式,我们下面给 Expr接口增加一个Check方法来对表达式语义树进行静态检查:

type Expr interface {
	Eval(env Env) float64
	Check(vars map[Var]bool) error
}

新建 check.go 文件实现接口方法:

package eval

import (
	"fmt"
	"strings"
)

func (v Var) Check(vars map[Var]bool) error {
	vars[v] = true
	return nil
}

func (literal) Check(vars map[Var]bool) error {
	return nil
}

func (u unary) Check(vars map[Var]bool) error {
	if !strings.ContainsRune("+-", u.op) {
		return fmt.Errorf("unexpected unary op %q", u.op)
	}
	return u.x.Check(vars)
}

func (b binary) Check(vars map[Var]bool) error {
	if !strings.ContainsRune("+-*/", b.op) {
		return fmt.Errorf("unexpected binary op %q", b.op)
	}
	if err := b.x.Check(vars); err != nil {
		return err
	}
	return b.y.Check(vars)
}

func (c call) Check(vars map[Var]bool) error {
	arity, ok := numParams[c.fn]
	if !ok {
		return fmt.Errorf("unknown function %q", c.fn)
	}
	if len(c.args) != arity {
		return fmt.Errorf("call to %s has %d args, want %d",
			c.fn, len(c.args), arity)
	}
	for _, arg := range c.args {
		if err := arg.Check(vars); err != nil {
			return err
		}
	}
	return nil
}

var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}

前面的测试相当于单元测试,可以验证模块的基本功能。下面的是覆盖率测试(coverage.go)。测试覆盖率的意义是,虽然它并不能检测出所有bug,但足够完善的覆盖率测试可以增强我们对程序可靠性的信心。

package eval

import (
	"fmt"
	"math"
	"testing"
)

func TestCoverage(t *testing.T) {
	var tests = []struct {
		input string
		env   Env
		want  string // expected error from Parse/Check or result from Eval
	}{
		{"x % 2", nil, "unexpected '%'"},
		{"!true", nil, "unexpected '!'"},
		{"log(10)", nil, `unknown function "log"`},
		{"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"},
		{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
		{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
		{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
	}

	for _, test := range tests {
		expr, err := Parse(test.input)
		if err == nil {
			err = expr.Check(map[Var]bool{})
		}
		if err != nil {
			if err.Error() != test.want {
				t.Errorf("%s: got %q, want %q", test.input, err, test.want)
			}
			continue
		}
		got := fmt.Sprintf("%.6g", expr.Eval(test.env))
		if got != test.want {
			t.Errorf("%s: %v => %s, want %s",
				test.input, test.env, got, test.want)
		}
	}
}

先确保所有的测试都可以通过:下面指定要运行的测试函数为 TestCoverage

zime@zime-virtual-machine:~/go/src/ch7eval$ go test -v -run=Coverage .
=== RUN   TestCoverage
--- PASS: TestCoverage (0.00s)
PASS
ok  	ch7eval	0.002s

要了解测试覆盖率工具的用法,可以 go  tool  cover 显示帮助信息。下面的命令用了 -coverprofile 标志参数,它通过在测试代码中插入生成钩子来统计覆盖率数据,即在运行每个测试前,它将待测试代码拷贝一份并做修改,在每个词法块都会设置一个布尔标志变量。当被修改后的被测代码运行退出时,将统计日志数据写入 c.out 文件,然后输出一个总结信息:

zime@zime-virtual-machine:~/go/src/ch7eval$ go test -run=Coverage -coverprofile=c.out .
ok  	ch7eval	0.003s	coverage: 76.4% of statements

可以用下面的 go tool cover 命令解读上述统计日志文件(将自动打开浏览器显示结果):

zime@zime-virtual-machine:~/go/src/ch7eval$ go tool cover -html=c.out

上图中,可以选择显示不同文件的测试覆盖情况。代码中绿色的表示测试覆盖到了,红色的则表示没有被覆盖到。这样,我们很容易添加测试来增加覆盖面(panic语句没有被执行到通常是合理的),但100%的测试覆盖率通常在实践中不可行。

如果我们额外加上 -covermode=count 标志参数,那么每个代码块中插入的不是布尔变量而是一个计数器。这样的话,统计日志中不仅可以知道代码块被执行了没有,还能知道哪些是被频繁执行的热点代码(从灰色到绿色,覆盖度递增):

zime@zime-virtual-machine:~/go/src/ch7eval$ go test -run=Coverage -covermode=count -coverprofile=c2.out .
ok  	ch7eval	0.003s	coverage: 76.4% of statements
zime@zime-virtual-machine:~/go/src/ch7eval$ go tool cover -html=c2.out

基准测试

基准测试相当于性能测试。golang中基准测试函数写法和普通测试函数类似,只是前缀是Benchmark,带一个 *testing.B 类型的参数

package word

import "unicode"

func IsPalindrome(s string) bool {
	var letters []rune
	for _, r := range s {
		if unicode.IsLetter(r) {
			letters = append(letters, unicode.ToLower(r))
		} else if unicode.Is(unicode.Han, r) {
			letters = append(letters, r)
		}
	}
	for i := range letters {
		if letters[i] != letters[len(letters)-1-i] {
			return false
		}
	}
	return true
}

上面的例子测试回文检测函数的性能。测试命令中 -bench 用正则方式指定要测试的函数,. 表示所有基准测试函数:

zime@zime-virtual-machine:~/go/src/ch11word$ go test -bench=.
goos: linux
goarch: amd64
pkg: ch11word
cpu: Intel(R) Core(TM) i5-4590S CPU @ 3.00GHz
BenchmarkIsPalindrome 	 3811491	       317.2 ns/op
PASS
ok  	ch11word	1.534s

在基准测试后,我们有时候会想到能否优化算法和程序,例如将 for i := range letters { 改写成 for i := 0; i < len(letters)/2; i++ {,但是,我们再次运行测试可以发现改善非常有限(离改善一半时间差很远),这样的优化没有多少意义。而且,有些场合,收益很低的优化反而导致代码复杂性增加不少。

zime@zime-virtual-machine:~/go/src/ch11word$ go test -bench=.
goos: linux
goarch: amd64
pkg: ch11word
cpu: Intel(R) Core(TM) i5-4590S CPU @ 3.00GHz
BenchmarkIsPalindrome 	 3324339	       304.3 ns/op
PASS
ok  	ch11word	1.382s

我们来进行另一种形式的优化:预先分配一个足够大的数组,从而后续append调用就不会反复内存分配了

func IsPalindrome(s string) bool {
	letters := make([]rune, 0, len(s))
	for _, r := range s {
		if unicode.IsLetter(r) {
			letters = append(letters, unicode.ToLower(r))
		} else if unicode.Is(unicode.Han, r) {
			letters = append(letters, r)
		}
	}
	for i := range letters {
		if letters[i] != letters[len(letters)-1-i] {
			return false
		}
	}
	return true
}
zime@zime-virtual-machine:~/go/src/ch11word$ go test -bench=.
goos: linux
goarch: amd64
pkg: ch11word
cpu: Intel(R) Core(TM) i5-4590S CPU @ 3.00GHz
BenchmarkIsPalindrome 	 6103443	       178.7 ns/op
PASS
ok  	ch11word	1.295s

可以发现,这个改进性能提升明显。用 -benchmem标志参数可以输出内存分配情况:

zime@zime-virtual-machine:~/go/src/ch11word$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: ch11word
cpu: Intel(R) Core(TM) i5-4590S CPU @ 3.00GHz
BenchmarkIsPalindrome 	 3321108	       327.5 ns/op	     248 B/op	       5 allocs/op
PASS
ok  	ch11word	1.459s
zime@zime-virtual-machine:~/go/src/ch11word$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: ch11word
cpu: Intel(R) Core(TM) i5-4590S CPU @ 3.00GHz
BenchmarkIsPalindrome 	 5932233	       183.9 ns/op	     128 B/op	       1 allocs/op
PASS
ok  	ch11word	1.301s

上面是优化前后的内存分配情况。

基准测试的结果是和数据规模有关的,下面的比较型的基准测试可以用于比较不同数量级的基准测试:

func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B)         { benchmark(b, 10) }
func Benchmark100(b *testing.B)        { benchmark(b, 100) }
func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }

剖析

基准测试往往是针对模块中的函数,是对特定操作的性能的衡量。我们希望整个程序运行得更快,但常常不知道从哪里开始优化——知道了瓶颈的大概位置再进行基准测试可能是准确的。性能剖析是从更宏观角度去观察程序的运行速度

zime@zime-virtual-machine:~/go/src/ch11word$ go test -run=NONE -bench=. -cpuprofile=cpu.out -memprofile=mem.out -blockprofile=block.out

上述命令会生成cpu剖析(最耗cpu时间的函数)、堆剖析(最耗内存的语句)、阻塞剖析(阻塞goroutine最久的操作)到相应文件(实际最好不要同时生成几个剖析文件,因为生成其中一个的分析操作可能影响另一个),还有 *.test 文件。接下来用 go tool pprof 命令解析性能文件:

zime@zime-virtual-machine:~/go/src/ch11word$ go tool pprof -text -nodecount=10 ./ch11word.test cpu.out 
File: ch11word.test
Type: cpu
Time: Mar 26, 2024 at 2:29pm (CST)
Duration: 1.65s, Total samples = 1450ms (87.66%)
Showing nodes accounting for 1150ms, 79.31% of 1450ms total
Showing top 10 nodes out of 68
      flat  flat%   sum%        cum   cum%
     350ms 24.14% 24.14%     1430ms 98.62%  ch11word.IsPalindrome
     210ms 14.48% 38.62%      620ms 42.76%  runtime.mallocgc
     180ms 12.41% 51.03%      920ms 63.45%  runtime.growslice
     140ms  9.66% 60.69%      140ms  9.66%  unicode.ToLower
      70ms  4.83% 65.52%       70ms  4.83%  runtime.nextFreeFast (inline)
      60ms  4.14% 69.66%       60ms  4.14%  runtime.memclrNoHeapPointers
      50ms  3.45% 73.10%       50ms  3.45%  runtime.scanblock
      30ms  2.07% 75.17%       30ms  2.07%  runtime.(*mspan).base (inline)
      30ms  2.07% 77.24%      120ms  8.28%  runtime.gcmarknewobject
      30ms  2.07% 79.31%       30ms  2.07%  runtime.markBits.setMarked (inline)

借助 GraphViz, pprof 可以图形化显示性能分析结果,这对复杂的性能问题更适用,因为上面那样的 -text 格式已经能帮助我们知道大部分问题。golang官方博客“Profiling Go Programs”有详细说明。

示例函数

示例函数也是被 go test 特殊对待的函数,函数名用 Example 作为前缀。示例函数没有函数参数和返回值,有点像是给人演示用法的main函数

func ExampleIsPalindrome() {
	fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
	fmt.Println(IsPalindrome("palindrome"))
	// Output:
	// true
	// false
}
zime@zime-virtual-machine:~/go/src/ch11word$ go test -run=IsPalindrome -v
=== RUN   TestIsPalindrome
--- PASS: TestIsPalindrome (0.00s)
=== RUN   ExampleIsPalindrome
--- PASS: ExampleIsPalindrome (0.00s)
PASS
ok  	ch11word	0.002s
zime@zime-virtual-machine:~/go/src/ch11word$ go test -run=ExampleIsPalindrome -v
=== RUN   ExampleIsPalindrome
--- PASS: ExampleIsPalindrome (0.00s)
PASS
ok  	ch11word	0.002s

需要注意的是:ExampleXxx 函数中的 // Output: 格式的注释后的注释行对应了前面部分的标准输出,即注释中就包含了用来核对的输出结果(必须格式对应)。如果没有这个注释部分,示例函数不会被执行(在GoLand IDE中表示运行的绿色三角形会消失)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/489885.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java常用IO流功能——字符流和缓冲流概述

前言&#xff1a; 整理下学习笔记&#xff0c;打好基础&#xff0c;daydayup! 之前说了下了IO流的概念&#xff0c;并整理了字节流&#xff0c;有需要的可以看这篇 java常用应用程序编程接口&#xff08;API&#xff09;——IO流概述及字节流的使用 字符流 FileReader(文件字…

鸿蒙TypeScript入门学习第一天【简单介绍】

1.TypeScript 教程 TypeScript 是 JavaScript 的一个超集&#xff0c;支持 ECMAScript 6 标准&#xff08;&#xff09;。 TypeScript 由微软开发的自由和开源的编程语言。 TypeScript 设计目标是开发大型应用&#xff0c;它可以编译成纯 JavaScript&#xff0c;编译出来的 …

BIM插件定制,你的功能由你说了算!python开发 | 图形引擎 | 几何引擎

各位CSDN的宝宝们~ 如果你对插件开发或BIM技术兴趣十足 有着无限的想法和创意 想解决日常使用BIM软件的难题&#xff0c;打破桎梏 却愁于没有实现的机会 现在&#xff0c;机会来了&#xff01; BIMBase插件命题征集活动已经开启&#xff01; &#xff08;没错就是征集&am…

AI时代-普通人的AI绘画工具对比(Midjouney与Stable Diffusion)

AI时代-普通人的AI绘画工具对比&#xff08;Midjouney与Stable Diffusion&#xff09; 前言1、基础对比Stable Diffusion&#xff08;SD&#xff09;SD界面安装与使用SD Midjouney&#xff08;MJ&#xff09; 2、硬件与运行要求对比Stable Diffusion硬件要求内存硬盘显卡 Midjo…

647. 回文子串

#动态规划法 class Solution:def countSubstrings(self, s: str) -> int:n len(s)#dp[i][j] [i,j]是否为回文串dp [[False]*n for _ in range(n)]res 0#dp[i][j]依赖于dp[i1][j-1]&#xff0c;所以i要从下往上遍历for i in range(n-1,-1,-1):for j in range(i,n):if s[i]…

基于java+springboot+vue实现的超市管理系统(文末源码+Lw+ppt)23-354

摘 要 系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对超市管理的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”…

神经网络代码实现(用手写数字识别数据集实验)

目录 一、前言 二、神经网络架构 三、算法实现 1、导入包 2、实现类 3、训练函数 4、权重参数矩阵初始化 5、参数矩阵变换向量 6、向量变换权重参数矩阵 7、进行梯度下降 7.1、损失函数 7.1.1、前向传播 7.2、反向传播 8、预测函数 四、完整代码 五、手写数字识别 一、前言 …

创建数组的时候,数组大小是确定数值和变量的不同情况

概要&#xff1a; 1、创将数组的时候&#xff0c;如果数组大小是确定数值 &#xff08;1&#xff09;数组所有元素默认是0 &#xff08;2&#xff09;可以通过大括号对元素进行赋值 int arr[3]{1,2,3}; int arr[10]{1}; //只将第一个元素赋值为1,其他元素依然是0 2、…

istio 设置 istio-proxy sidecar 的 resource 的 limit 和 request

方式一 修改 configmap 查看当前 sidecar 的 cpu 和 memory 的配额 在 istio-sidecar-injector 中查找,修改后重启 pod 可以生效(下面那个 proxy_init 配置不管,不知道是干嘛的) 方式二 如果是通过 iop 安装的 istio,可以修改 iop 文件中的配置 spec:values:global:…

静态住宅IP VS 动态住宅IP,怎么选择?

在进行海外 IP 代理时&#xff0c;了解动态住宅 IP 和静态住宅 IP 的区别以及如何选择合适的类型非常重要。本文将介绍精态住宅 IP 特点和&#xff0c;并提供选择建议&#xff0c;帮助您根据需求做出明智的决策。 静态住宅 IP 的特点 静态住宅 IP 是指 IP 地址在一段时间内保…

【Java程序设计】【C00360】基于Springboot的考研互助交流平台(有论文)

基于Springboot的考研互助交流平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以及i…

Cadence——生成Gerber制板文件

软件版本&#xff1a;Cadence SPB OrCAD Allegro 16.6 打开Allegro PCB Designer 选择如下选项&#x1f447; 点击 File–>Open&#xff0c;找到对应的.brd文件 电气错误的检查&#xff1a;点击 Display–>Status&#xff0c;全为绿色则没有错误 按照不同的项目来进…

【SQL】1517. 查找拥有有效邮箱的用户(正则表达式regexp)

前述 sql-正则表达式SQL学习笔记 – REGEXP 题目描述 leetcode 题目&#xff1a;1517. 查找拥有有效邮箱的用户 Code select * from Users where mail regexp ^[a-zA-Z][a-zA-Z0-9_.-]*leetcode\\.com$图片引用自 MySQL正则表达式

C++中的内存分区

栈&#xff1a;在执行函数时&#xff0c;函数内局部变量的存储单元都可以在栈上创建&#xff0c;函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中&#xff0c;效率很高&#xff0c;但是分配的内存容量有限 堆&#xff1a;就是那些由 new分配的内…

【Python版】手把手带你如何进行Mock测试

什么是mock&#xff1f; mock测试是以可控的方式模拟真实的对象行为。程序员通常创造模拟对象来测试对象本身该具备的行为&#xff0c;很类似汽车设计者使用碰撞测试假人来模拟车辆碰撞中人的动态行为 为什么要使用Mock&#xff1f; 之所以使用mock测试&#xff0c;是因为真…

matplotlib查询当前系统所有字体

电脑里有这个字体但是不代表matplotlib里也有这个字体&#xff0c;所以解决matplotlib中的中文显示问题主要就是要找到它所内置支持的字体&#xff0c;那么我们首先查看一下它的内置字体&#xff0c;运行以下代码查看所支持的字体 # 查询当前系统所有字体 from matplotlib.fon…

老程序员帮公司面试java程序员,工资都在1万5左右

案例1&#xff08;电商项目经验&#xff09; 培训机构的最爱电商项目&#xff0c;有木有 项目具体模块都讲不清楚&#xff0c;面试直接下课 项目核心业务表都记不住&#xff0c;很难让面试官相信你真的开发过 面试3年电商经验java开发&#xff0c;坚持看完最后有我公司招聘要…

kettle--资源库

1、数据库资源库&#xff08;使用的会比较频繁&#xff09; 作用&#xff1a;数据库资源库就是将转换和作业相关的信息保存到数据库中&#xff0c;当团队中需要使用到的时候都可以去使用该数据。 创建数据库资源库的一般流程&#xff1a; 建立数据的连接后查看数据库中对应的…

【超全详解一文搞懂】Scala基础

目录 Scala 01 —— Scala基础一、搭建Scala开发环境安装Scala编译器在IDEA中进行scala编码 二、Scala简介与概述Scala简介Scala概述Scala代码规范 三、理解Scala变量与数据类型Scala的变量与常量Scala和Java变量的区别 Scala的数据类型 四、Scala的程序逻辑1.表达式2.运算符3.…
最新文章