├── README.md ├── basic.md ├── eval ├── ast.go ├── check.go ├── coverage_test.go ├── eval.go ├── parse.go └── print.go ├── http_test.md ├── httptest ├── client │ └── mockserver_test.go └── server │ └── mockclient_test.go ├── mock.md ├── mock ├── company.go ├── mk │ └── mock_Talker.go ├── mock_talker_test.go ├── person.go └── talker.go ├── table_driven_test.md ├── tdt ├── tdt.go └── tdt_test.go ├── testify.md ├── testify ├── assert │ ├── assert_test.go │ └── require_test.go ├── mock │ ├── mocks │ │ └── Stringer.go │ ├── string.go │ └── string_test.go └── suite │ └── suite_test.go ├── word ├── bench_test.go ├── word.go └── word_test.go └── 单元测试建议.pdf /README.md: -------------------------------------------------------------------------------- 1 | ## Testing in golang 2 | 3 | + [Go单元测试基础](./basic.md) 4 | + [表格驱动测试](./table_driven_test.md) 5 | + [go-mock](./mock.md) 6 | + [Testify](./testify.md) 7 | + [HTTP-Test](./http_test.md) 8 | + [单元测试指导建议](./单元测试建议.pdf) 9 | + [goconvey](https://github.com/smartystreets/goconvey) 10 | + [锁竞争检测](https://github.com/sasha-s/go-deadlock) 11 | + 用sync "github.com/sasha-s/go-deadlock" 替换 "sync" 执行单元测试,检测死锁问题 12 | + [《Golang基于Gitlab CI/CD部署方案》](http://www.chairis.cn/blog/article/96) 13 | + [《Geotechnical 单元测试准则》](https://github.com/yangyubo/zh-unit-testing-guidelines) 14 | + [《Golang 代码质量持续检测》](https://github.com/developer-learning/night-reading-go/blob/master/articles/sonarqube-for-golang/sonarqube-for-golang.md) 15 | 16 | #### 参考资料 17 | + [go-testing-toolbox](https://nathany.com/go-testing-toolbox/) 18 | + 《阿里巴巴Java开发手册》 19 | + [《都100%代码覆盖了,还会有什么问题?》](http://insights.thoughtworks.cn/code-coverage-2/) 20 | + [《Golang 单元测试详尽指引》](https://mp.weixin.qq.com/s/eAptnygPQcQ5Ex8-6l0byA) 推荐 21 | 22 | 23 | -------------------------------------------------------------------------------- /basic.md: -------------------------------------------------------------------------------- 1 | ## Go单元测试基础 2 | ​ Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。在包目录内,以_test.go为后缀名的源文件都是go test的一部分,而不是go build的构建部分。 3 | 4 | ```shell 5 | # 查看帮助 6 | go test -h 7 | # 命令形式 8 | go test [build/test flags] [packages] [build/test flags & test binary flags] 9 | ``` 10 | 11 | #### 单元测试文件 12 | 13 | + 文件名必须是`_test.go`结尾的,这样在执行`go test`的时候才会执行到相应的代码。 14 | 15 | - 你必须`import testing`这个包。 16 | - 所有的测试用例函数必须是`Test`开头。 17 | - 测试用例会按照源代码中写的顺序依次执行。 18 | - 测试函数`TestX()`的参数是`testing.T`,我们可以使用该类型来记录错误或者是测试状态。 19 | - 测试格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如`Testintdiv`是错误的函数名。 20 | - 函数中通过调用`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,说明测试不通过,调用`Log`方法用来记录测试的信息。 21 | 22 | #### 测试函数 23 | 24 | + 每个测试函数必须导入testing包,测试函数有如下签名: 25 | 26 | ```go 27 | func TestXXX(t *testing.T) { 28 | // ... 29 | } 30 | ``` 31 | 32 | 33 | + 我们使用[《The Go Programming Language》](http://gopl.io/) 中的例子说明,测试代码我们放置在word目录下面。 34 | 35 | ```go 36 | // 被测试的函数是IsPalindrome,功能是判断传入的字符窜是否对称 37 | func TestPalindrome(t *testing.T) { 38 | if !IsPalindrome("detartrated") { 39 | t.Error(`IsPalindrome("detartrated") = false`) 40 | } 41 | if !IsPalindrome("kayak") { 42 | t.Error(`IsPalindrome("kayak") = false`) 43 | } 44 | } 45 | 46 | func TestNonPalindrome(t *testing.T) { 47 | if IsPalindrome("palindrome") { 48 | t.Error(`IsPalindrome("palindrome") = true`) 49 | } 50 | } 51 | ``` 52 | 53 | + 运行测试 54 | 55 | ```go 56 | go test ./word // 指定目录 57 | go test word // 指定包名(包需要在GOPTAH/src或者GOROOT/src目录下面) 58 | 59 | cd word // 进入包测试 60 | go test 61 | 62 | // 输出结果 63 | PASS 64 | ok _/home/frank/workspace/testing/word 0.004s 65 | ``` 66 | 67 | 注:参数`-v`可用于打印每个测试函数的名字和运行时间: 68 | 69 | + 添加异常测试 70 | 71 | ```go 72 | func TestCanalPalindrome(t *testing.T) { 73 | input := "A man, a plan, a canal: Panama" 74 | if !IsPalindrome(input) { 75 | t.Errorf(`IsPalindrome(%q) = false`, input) 76 | } 77 | } 78 | ``` 79 | 80 | 参数`-run`对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被`go test`测试命令运行: 81 | 82 | ```shell 83 | go test -timeout 30s -tags -v -run ^TestCanalPalindrome$ 84 | 85 | // 异常输出 86 | --- FAIL: TestCanalPalindrome (0.00s) 87 | word_test.go:23: IsPalindrome("A man, a plan, a canal: Panama") = false 88 | FAIL 89 | exit status 1 90 | FAIL _/home/frank/workspace/testing/word 0.003s 91 | ``` 92 | 93 | #### 覆盖率测试 94 | 95 | ​ **语句的覆盖率**是指在测试中至少被运行一次的代码占总代码数的比例。我们使用`go test`命令中集成的测试覆盖率工具,来度量下面代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。 96 | 97 | + 我们使用gopl中的eval包进行说明。我们放置在eval目录中,以表格驱动的测试进行。 98 | 99 | ```shell 100 | // 我们先保证测试可以通过 101 | go test -v 102 | // 生成覆盖率测试报告 103 | go test -coverprofile=c.out 104 | // 转换成html(需要在GOPATH/src目录下面) 105 | go tool cover -html=c.out -o coverage.html 106 | ``` 107 | 108 | + [Testivus On Test Coverage](http://blog.csdn.net/win_lin/article/details/74551803) 109 | 110 | #### 基准测试 111 | 112 | ​ 基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个`*testing.B`类型的参数;`*testing.B`参数除了提供和`*testing.T`类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。 113 | 114 | ``` 115 | func BenchmarkIsPalindrome(b *testing.B) { 116 | for i := 0; i < b.N; i++ { 117 | IsPalindrome("A man, a plan, a canal: Panama") 118 | } 119 | } 120 | ``` 121 | 122 | ```go 123 | go test -bench=. -run BenchmarkIsPalindrome 124 | go test -bench=. -run BenchmarkIsPalindrome -benchmem 125 | ...... 126 | ``` 127 | 128 | #### 参考资料 129 | 130 | + [《Go怎么写测试用例》](https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/11.3.md) 131 | + [《编写可测试的Go代码》](http://tabalt.net/blog/golang-testing/) 132 | + [《golang test说明解读》](http://www.cnblogs.com/yjf512/archive/2013/01/22/2870927.html) 133 | + [《Go单元测试》](https://rootsongjc.github.io/go-practice/docs/go_unit_test.html) 134 | + [《Go语言圣经》](http://books.studygolang.com/gopl-zh/) -------------------------------------------------------------------------------- /eval/ast.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | package eval 5 | 6 | // An Expr is an arithmetic expression. 7 | type Expr interface { 8 | // Eval returns the value of this Expr in the environment env. 9 | Eval(env Env) float64 10 | // Check reports errors in this Expr and adds its Vars to the set. 11 | Check(vars map[Var]bool) error 12 | } 13 | 14 | //!+ast 15 | 16 | // A Var identifies a variable, e.g., x. 17 | type Var string 18 | 19 | // A literal is a numeric constant, e.g., 3.141. 20 | type literal float64 21 | 22 | // A unary represents a unary operator expression, e.g., -x. 23 | type unary struct { 24 | op rune // one of '+', '-' 25 | x Expr 26 | } 27 | 28 | // A binary represents a binary operator expression, e.g., x+y. 29 | type binary struct { 30 | op rune // one of '+', '-', '*', '/' 31 | x, y Expr 32 | } 33 | 34 | // A call represents a function call expression, e.g., sin(x). 35 | type call struct { 36 | fn string // one of "pow", "sin", "sqrt" 37 | args []Expr 38 | } 39 | 40 | //!-ast 41 | -------------------------------------------------------------------------------- /eval/check.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | package eval 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | //!+Check 12 | 13 | func (v Var) Check(vars map[Var]bool) error { 14 | vars[v] = true 15 | return nil 16 | } 17 | 18 | func (literal) Check(vars map[Var]bool) error { 19 | return nil 20 | } 21 | 22 | func (u unary) Check(vars map[Var]bool) error { 23 | if !strings.ContainsRune("+-", u.op) { 24 | return fmt.Errorf("unexpected unary op %q", u.op) 25 | } 26 | return u.x.Check(vars) 27 | } 28 | 29 | func (b binary) Check(vars map[Var]bool) error { 30 | if !strings.ContainsRune("+-*/", b.op) { 31 | return fmt.Errorf("unexpected binary op %q", b.op) 32 | } 33 | if err := b.x.Check(vars); err != nil { 34 | return err 35 | } 36 | return b.y.Check(vars) 37 | } 38 | 39 | func (c call) Check(vars map[Var]bool) error { 40 | arity, ok := numParams[c.fn] 41 | if !ok { 42 | return fmt.Errorf("unknown function %q", c.fn) 43 | } 44 | if len(c.args) != arity { 45 | return fmt.Errorf("call to %s has %d args, want %d", 46 | c.fn, len(c.args), arity) 47 | } 48 | for _, arg := range c.args { 49 | if err := arg.Check(vars); err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1} 57 | 58 | //!-Check 59 | -------------------------------------------------------------------------------- /eval/coverage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | package eval 5 | 6 | import ( 7 | "fmt" 8 | "math" 9 | "testing" 10 | ) 11 | 12 | //!+TestCoverage 13 | func TestCoverage(t *testing.T) { 14 | var tests = []struct { 15 | input string 16 | env Env 17 | want string // expected error from Parse/Check or result from Eval 18 | }{ 19 | {"x % 2", nil, "unexpected '%'"}, 20 | {"!true", nil, "unexpected '!'"}, 21 | {"log(10)", nil, `unknown function "log"`}, 22 | {"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"}, 23 | {"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"}, 24 | {"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"}, 25 | {"5 / 9 * (F - 32)", Env{"F": -40}, "-40"}, 26 | } 27 | 28 | for _, test := range tests { 29 | expr, err := Parse(test.input) 30 | if err == nil { 31 | err = expr.Check(map[Var]bool{}) 32 | } 33 | if err != nil { 34 | if err.Error() != test.want { 35 | t.Errorf("%s: got %q, want %q", test.input, err, test.want) 36 | } 37 | continue 38 | } 39 | 40 | got := fmt.Sprintf("%.6g", expr.Eval(test.env)) 41 | if got != test.want { 42 | t.Errorf("%s: %v => %s, want %s", 43 | test.input, test.env, got, test.want) 44 | } 45 | } 46 | } 47 | 48 | //!-TestCoverage 49 | -------------------------------------------------------------------------------- /eval/eval.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | // See page 198. 5 | 6 | // Package eval provides an expression evaluator. 7 | package eval 8 | 9 | import ( 10 | "fmt" 11 | "math" 12 | ) 13 | 14 | //!+env 15 | 16 | type Env map[Var]float64 17 | 18 | //!-env 19 | 20 | //!+Eval1 21 | 22 | func (v Var) Eval(env Env) float64 { 23 | return env[v] 24 | } 25 | 26 | func (l literal) Eval(_ Env) float64 { 27 | return float64(l) 28 | } 29 | 30 | //!-Eval1 31 | 32 | //!+Eval2 33 | 34 | func (u unary) Eval(env Env) float64 { 35 | switch u.op { 36 | case '+': 37 | return +u.x.Eval(env) 38 | case '-': 39 | return -u.x.Eval(env) 40 | } 41 | panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) 42 | } 43 | 44 | func (b binary) Eval(env Env) float64 { 45 | switch b.op { 46 | case '+': 47 | return b.x.Eval(env) + b.y.Eval(env) 48 | case '-': 49 | return b.x.Eval(env) - b.y.Eval(env) 50 | case '*': 51 | return b.x.Eval(env) * b.y.Eval(env) 52 | case '/': 53 | return b.x.Eval(env) / b.y.Eval(env) 54 | } 55 | panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) 56 | } 57 | 58 | func (c call) Eval(env Env) float64 { 59 | switch c.fn { 60 | case "pow": 61 | return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env)) 62 | case "sin": 63 | return math.Sin(c.args[0].Eval(env)) 64 | case "sqrt": 65 | return math.Sqrt(c.args[0].Eval(env)) 66 | } 67 | panic(fmt.Sprintf("unsupported function call: %s", c.fn)) 68 | } 69 | 70 | //!-Eval2 71 | -------------------------------------------------------------------------------- /eval/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | package eval 5 | 6 | import ( 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | "text/scanner" 11 | ) 12 | 13 | // ---- lexer ---- 14 | 15 | // This lexer is similar to the one described in Chapter 13. 16 | type lexer struct { 17 | scan scanner.Scanner 18 | token rune // current lookahead token 19 | } 20 | 21 | func (lex *lexer) next() { lex.token = lex.scan.Scan() } 22 | func (lex *lexer) text() string { return lex.scan.TokenText() } 23 | 24 | type lexPanic string 25 | 26 | // describe returns a string describing the current token, for use in errors. 27 | func (lex *lexer) describe() string { 28 | switch lex.token { 29 | case scanner.EOF: 30 | return "end of file" 31 | case scanner.Ident: 32 | return fmt.Sprintf("identifier %s", lex.text()) 33 | case scanner.Int, scanner.Float: 34 | return fmt.Sprintf("number %s", lex.text()) 35 | } 36 | return fmt.Sprintf("%q", rune(lex.token)) // any other rune 37 | } 38 | 39 | func precedence(op rune) int { 40 | switch op { 41 | case '*', '/': 42 | return 2 43 | case '+', '-': 44 | return 1 45 | } 46 | return 0 47 | } 48 | 49 | // ---- parser ---- 50 | 51 | // Parse parses the input string as an arithmetic expression. 52 | // 53 | // expr = num a literal number, e.g., 3.14159 54 | // | id a variable name, e.g., x 55 | // | id '(' expr ',' ... ')' a function call 56 | // | '-' expr a unary operator (+-) 57 | // | expr '+' expr a binary operator (+-*/) 58 | // 59 | func Parse(input string) (_ Expr, err error) { 60 | defer func() { 61 | switch x := recover().(type) { 62 | case nil: 63 | // no panic 64 | case lexPanic: 65 | err = fmt.Errorf("%s", x) 66 | default: 67 | // unexpected panic: resume state of panic. 68 | panic(x) 69 | } 70 | }() 71 | lex := new(lexer) 72 | lex.scan.Init(strings.NewReader(input)) 73 | lex.scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats 74 | lex.next() // initial lookahead 75 | e := parseExpr(lex) 76 | if lex.token != scanner.EOF { 77 | return nil, fmt.Errorf("unexpected %s", lex.describe()) 78 | } 79 | return e, nil 80 | } 81 | 82 | func parseExpr(lex *lexer) Expr { return parseBinary(lex, 1) } 83 | 84 | // binary = unary ('+' binary)* 85 | // parseBinary stops when it encounters an 86 | // operator of lower precedence than prec1. 87 | func parseBinary(lex *lexer, prec1 int) Expr { 88 | lhs := parseUnary(lex) 89 | for prec := precedence(lex.token); prec >= prec1; prec-- { 90 | for precedence(lex.token) == prec { 91 | op := lex.token 92 | lex.next() // consume operator 93 | rhs := parseBinary(lex, prec+1) 94 | lhs = binary{op, lhs, rhs} 95 | } 96 | } 97 | return lhs 98 | } 99 | 100 | // unary = '+' expr | primary 101 | func parseUnary(lex *lexer) Expr { 102 | if lex.token == '+' || lex.token == '-' { 103 | op := lex.token 104 | lex.next() // consume '+' or '-' 105 | return unary{op, parseUnary(lex)} 106 | } 107 | return parsePrimary(lex) 108 | } 109 | 110 | // primary = id 111 | // | id '(' expr ',' ... ',' expr ')' 112 | // | num 113 | // | '(' expr ')' 114 | func parsePrimary(lex *lexer) Expr { 115 | switch lex.token { 116 | case scanner.Ident: 117 | id := lex.text() 118 | lex.next() // consume Ident 119 | if lex.token != '(' { 120 | return Var(id) 121 | } 122 | lex.next() // consume '(' 123 | var args []Expr 124 | if lex.token != ')' { 125 | for { 126 | args = append(args, parseExpr(lex)) 127 | if lex.token != ',' { 128 | break 129 | } 130 | lex.next() // consume ',' 131 | } 132 | if lex.token != ')' { 133 | msg := fmt.Sprintf("got %q, want ')'", lex.token) 134 | panic(lexPanic(msg)) 135 | } 136 | } 137 | lex.next() // consume ')' 138 | return call{id, args} 139 | 140 | case scanner.Int, scanner.Float: 141 | f, err := strconv.ParseFloat(lex.text(), 64) 142 | if err != nil { 143 | panic(lexPanic(err.Error())) 144 | } 145 | lex.next() // consume number 146 | return literal(f) 147 | 148 | case '(': 149 | lex.next() // consume '(' 150 | e := parseExpr(lex) 151 | if lex.token != ')' { 152 | msg := fmt.Sprintf("got %s, want ')'", lex.describe()) 153 | panic(lexPanic(msg)) 154 | } 155 | lex.next() // consume ')' 156 | return e 157 | } 158 | msg := fmt.Sprintf("unexpected %s", lex.describe()) 159 | panic(lexPanic(msg)) 160 | } 161 | -------------------------------------------------------------------------------- /eval/print.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan. 2 | // License: https://creativecommons.org/licenses/by-nc-sa/4.0/ 3 | 4 | package eval 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | ) 10 | 11 | // Format formats an expression as a string. 12 | // It does not attempt to remove unnecessary parens. 13 | func Format(e Expr) string { 14 | var buf bytes.Buffer 15 | write(&buf, e) 16 | return buf.String() 17 | } 18 | 19 | func write(buf *bytes.Buffer, e Expr) { 20 | switch e := e.(type) { 21 | case literal: 22 | fmt.Fprintf(buf, "%g", e) 23 | 24 | case Var: 25 | fmt.Fprintf(buf, "%s", e) 26 | 27 | case unary: 28 | fmt.Fprintf(buf, "(%c", e.op) 29 | write(buf, e.x) 30 | buf.WriteByte(')') 31 | 32 | case binary: 33 | buf.WriteByte('(') 34 | write(buf, e.x) 35 | fmt.Fprintf(buf, " %c ", e.op) 36 | write(buf, e.y) 37 | buf.WriteByte(')') 38 | 39 | case call: 40 | fmt.Fprintf(buf, "%s(", e.fn) 41 | for i, arg := range e.args { 42 | if i > 0 { 43 | buf.WriteString(", ") 44 | } 45 | write(buf, arg) 46 | } 47 | buf.WriteByte(')') 48 | 49 | default: 50 | panic(fmt.Sprintf("unknown Expr: %T", e)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /http_test.md: -------------------------------------------------------------------------------- 1 | ## HTTP Test 2 | 3 | + Golang为我们提供了httptest包进行http请求和应答的测试,不需要发起真正的请求。 4 | + http测试分成两个部分: 5 | + 客户端测试:针对客户端测试的时候,我们需要自己构建MockServer进行应答。 6 | + 服务端测试:针对服务端测试的时候,我们需要自己构建MockClient发起请求。 7 | 8 | #### MockServer 9 | 10 | ```go 11 | func TestMockServer(t *testing.T) { 12 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | fmt.Fprintln(w, "Hello, client") 14 | })) 15 | defer ts.Close() 16 | 17 | t.Logf("ts.Url : %s", ts.URL) 18 | res, err := http.Get(ts.URL) // 注意:这边使用的是ts.URL 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | greeting, err := ioutil.ReadAll(res.Body) 23 | res.Body.Close() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | t.Logf("%s", greeting) 29 | } 30 | ``` 31 | 32 | #### MockClient 33 | 34 | ```go 35 | func TestMockClient(t *testing.T) { 36 | // 需要被测试的handler 37 | handler := func(w http.ResponseWriter, r *http.Request) { 38 | io.WriteString(w, "Hello World!") 39 | } 40 | 41 | req := httptest.NewRequest("GET", "http://example.com", nil) 42 | w := httptest.NewRecorder() 43 | 44 | // 调用处理逻辑 45 | handler(w, req) 46 | 47 | resp := w.Result() 48 | body, _ := ioutil.ReadAll(resp.Body) 49 | 50 | fmt.Println(resp.StatusCode) 51 | fmt.Println(resp.Header.Get("Content-Type")) 52 | fmt.Println(string(body)) 53 | } 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /httptest/client/mockserver_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | // 测试客户端代码,所以需要mock服务端 12 | 13 | func TestMockServer(t *testing.T) { 14 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | fmt.Fprintln(w, "Hello, client") 16 | })) 17 | defer ts.Close() 18 | 19 | t.Logf("ts.Url : %s", ts.URL) 20 | res, err := http.Get(ts.URL) // 注意:这边使用的是ts.URL 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | greeting, err := ioutil.ReadAll(res.Body) 25 | res.Body.Close() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | t.Logf("%s", greeting) 31 | } 32 | -------------------------------------------------------------------------------- /httptest/server/mockclient_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | func TestMockClient(t *testing.T) { 13 | // 需要被测试的handler 14 | handler := func(w http.ResponseWriter, r *http.Request) { 15 | io.WriteString(w, "Hello World!") 16 | } 17 | 18 | req := httptest.NewRequest("GET", "http://example.com", nil) 19 | w := httptest.NewRecorder() 20 | 21 | // 调用处理逻辑 22 | handler(w, req) 23 | 24 | resp := w.Result() 25 | body, _ := ioutil.ReadAll(resp.Body) 26 | 27 | fmt.Println(resp.StatusCode) 28 | fmt.Println(resp.Header.Get("Content-Type")) 29 | fmt.Println(string(body)) 30 | } 31 | 32 | func TestServerReadErr(t *testing.T) { 33 | handler := func(w http.ResponseWriter, r *http.Request) { 34 | data, err := ioutil.ReadAll(r.Body) 35 | if err != nil { 36 | w.WriteHeader(http.StatusBadRequest) 37 | return 38 | } 39 | io.WriteString(w, fmt.Sprintf("Hello %s!", string(data))) 40 | } 41 | 42 | req := httptest.NewRequest("GET", "http://example.com", nil) 43 | w := httptest.NewRecorder() 44 | 45 | // 调用处理逻辑 46 | handler(w, req) 47 | } 48 | -------------------------------------------------------------------------------- /mock.md: -------------------------------------------------------------------------------- 1 | ## Mock 2 | 3 | ​ Mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。 4 | 5 | + mock对象使用范畴 6 | 7 | 真实对象具有不可确定的行为,产生不可预测的效果,真实对象很难被创建的、真实对象的某些行为很难被触发、真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等。 8 | 9 | + 使用mock对象测试的关键步骤 10 | 11 | 使用一个接口来描述这个对象。在产品代码中实现这个接口,在测试代码中实现这个接口,在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是mock对象。 12 | 13 | #### gomock 14 | 15 | + [gomock](https://github.com/golang/mock) golang/mock下面的mock包,目前还没有发布release版本。 16 | 17 | + 创建接口 18 | 19 | ```go 20 | type Talker interface { 21 | SayHello(word string)(response string) 22 | } 23 | ``` 24 | 25 | + 生成mock程序 26 | 27 | ```shell 28 | mkdir mk 29 | mockgen -source=talker.go > mk/mock_Talker.go 30 | ``` 31 | 32 | ```go 33 | // MockTalker is a mock of Talker interface 34 | type MockTalker struct { 35 | ctrl *gomock.Controller 36 | recorder *MockTalkerMockRecorder 37 | } 38 | // MockTalkerMockRecorder is the mock recorder for MockTalker 39 | type MockTalkerMockRecorder struct { 40 | mock *MockTalker 41 | } 42 | // NewMockTalker creates a new mock instance 43 | func NewMockTalker(ctrl *gomock.Controller) *MockTalker { 44 | mock := &MockTalker{ctrl: ctrl} 45 | mock.recorder = &MockTalkerMockRecorder{mock} 46 | return mock 47 | } 48 | // EXPECT returns an object that allows the caller to indicate expected use 49 | func (_m *MockTalker) EXPECT() *MockTalkerMockRecorder { 50 | return _m.recorder 51 | } 52 | // SayHello mocks base method 53 | func (_m *MockTalker) SayHello(word string) string { 54 | ret := _m.ctrl.Call(_m, "SayHello", word) 55 | ret0, _ := ret[0].(string) 56 | return ret0 57 | } 58 | // SayHello indicates an expected call of SayHello 59 | func (_mr *MockTalkerMockRecorder) SayHello(arg0 interface{}) *gomock.Call { 60 | return _mr.mock.ctrl.RecordCall(_mr.mock, "SayHello", arg0) 61 | } 62 | ``` 63 | 64 | + 测试程序 65 | 66 | ```go 67 | func TestCompany_Meeting(t *testing.T) { 68 | ctl := gomock.NewController(t) 69 | // 创建一个mock对象 70 | mock_talker := mk.NewMockTalker(ctl) 71 | // 设置方法的返回值 72 | mock_talker.EXPECT().SayHello(gomock.Eq("王尼玛")).Return("这是自定义的返回值,可以是任意类型。") 73 | 74 | // 传入mock对象 75 | company := NewCompany(mock_talker) 76 | t.Log(company.Meeting("王尼玛")) 77 | } 78 | ``` 79 | 80 | #### 参考资料 81 | 82 | + [用gomock进行mock测试](https://segmentfault.com/a/1190000009894570) 83 | + [Go单元测试进阶篇](http://blog.csdn.net/qian_xiaoqian/article/details/54344856) 84 | + [GOLANG最容易做测试MOCK](http://blog.csdn.net/win_lin/article/details/72967636) -------------------------------------------------------------------------------- /mock/company.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | type Company struct { 4 | Usher Talker 5 | } 6 | 7 | func NewCompany(t Talker) *Company{ 8 | return &Company{ 9 | Usher:t, 10 | } 11 | } 12 | 13 | func ( c *Company) Meeting(gusetName string)string{ 14 | return c.Usher.SayHello(gusetName) 15 | } 16 | -------------------------------------------------------------------------------- /mock/mk/mock_Talker.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: talker.go 3 | 4 | package mk 5 | 6 | import ( 7 | gomock "github.com/golang/mock/gomock" 8 | ) 9 | 10 | // MockTalker is a mock of Talker interface 11 | type MockTalker struct { 12 | ctrl *gomock.Controller 13 | recorder *MockTalkerMockRecorder 14 | } 15 | 16 | // MockTalkerMockRecorder is the mock recorder for MockTalker 17 | type MockTalkerMockRecorder struct { 18 | mock *MockTalker 19 | } 20 | 21 | // NewMockTalker creates a new mock instance 22 | func NewMockTalker(ctrl *gomock.Controller) *MockTalker { 23 | mock := &MockTalker{ctrl: ctrl} 24 | mock.recorder = &MockTalkerMockRecorder{mock} 25 | return mock 26 | 27 | 28 | 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (_m *MockTalker) EXPECT() *MockTalkerMockRecorder { 33 | return _m.recorder 34 | } 35 | 36 | // SayHello mocks base method 37 | func (_m *MockTalker) SayHello(word string) string { 38 | ret := _m.ctrl.Call(_m, "SayHello", word) 39 | ret0, _ := ret[0].(string) 40 | return ret0 41 | } 42 | 43 | // SayHello indicates an expected call of SayHello 44 | func (_mr *MockTalkerMockRecorder) SayHello(arg0 interface{}) *gomock.Call { 45 | return _mr.mock.ctrl.RecordCall(_mr.mock, "SayHello", arg0) 46 | } 47 | -------------------------------------------------------------------------------- /mock/mock_talker_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "testing" 5 | "github.com/golang/mock/gomock" 6 | mk "./mk" 7 | ) 8 | 9 | func TestCompany_Meeting(t *testing.T) { 10 | ctl := gomock.NewController(t) 11 | mock_talker := mk.NewMockTalker(ctl) 12 | mock_talker.EXPECT().SayHello(gomock.Eq("王尼玛")).Return("这是自定义的返回值,可以是任意类型。") 13 | 14 | company := NewCompany(mock_talker) 15 | t.Log(company.Meeting("王尼玛")) 16 | } 17 | -------------------------------------------------------------------------------- /mock/person.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | 4 | 5 | import "fmt" 6 | 7 | type Person struct{ 8 | name string 9 | } 10 | 11 | func NewPerson(name string)*Person{ 12 | return &Person{ 13 | name:name, 14 | } 15 | } 16 | 17 | 18 | func (p *Person)SayHello(name string)string { 19 | return fmt.Sprintf("Hello %s, welcome come to our company, my name is %s",name,p.name) 20 | } 21 | -------------------------------------------------------------------------------- /mock/talker.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | type Talker interface { 4 | SayHello(word string)(response string) 5 | } -------------------------------------------------------------------------------- /table_driven_test.md: -------------------------------------------------------------------------------- 1 | ## 表格驱动测试 2 | ​ TDT也叫表格驱动方法,有时也被归为关键字驱动测试(keyword-driven testing,是针对自动化测试的软件测试方法,它将创建测试程序的步骤分为规划及实现二个阶段。Go官方库中有些测试就使用了这种测试方法。 3 | 4 | ​ TDT中每个表格项就是一个完整的test case,包含输入和期望的输出,有时候还会加一些额外的信息比如测试名称。如果你发现你的测试中经常copy/paste操作,你就可以考虑把它们改造成TDT。测试代码就一块,但是可以测试表格中的每一项。 5 | 6 | #### [gotests](https://github.com/cweill/gotests) 7 | 8 | ​ 自动生成Table-Drived-Tests测试代码的工具。 9 | 10 | + 我们创建tdt目录,内容如下: 11 | 12 | ```go 13 | func Add(a, b int) int { 14 | return a + b 15 | } 16 | 17 | func Min(nums []int) int { 18 | var rst int 19 | for i := range nums { 20 | if rst > nums[i] { 21 | rst = nums[i] 22 | } 23 | } 24 | return rst 25 | } 26 | 27 | func Max(nums []int) int { 28 | var rst int 29 | for i := range nums { 30 | if rst > nums[i] { 31 | rst = nums[i] 32 | } 33 | } 34 | return rst 35 | } 36 | ``` 37 | 38 | ``` 39 | // 生成上述代码的表格驱动测试代码,文件名为tdt_test.go 40 | gotests -all -w tdt.go 41 | 42 | // Visual Studio Code右键也可以自动生成测试代码 43 | ``` 44 | 45 | 以Add为例,分析生成的代码 46 | 47 | ```go 48 | func TestAdd(t *testing.T) { 49 | // 需要传入的参数 50 | type args struct { 51 | a int 52 | b int 53 | } 54 | tests := []struct { 55 | name string // 测试的名字 56 | args args // 需要传入的参数 57 | want int // 希望获取到的结果 58 | }{ 59 | // TODO: Add test cases. 60 | // 添加自己的测试例子 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | if got := Add(tt.args.a, tt.args.b); got != tt.want { 65 | // 如果测试结果不是自己的预期,那么报错。 66 | t.Errorf("Add() = %v, want %v", got, tt.want) 67 | } 68 | }) 69 | } 70 | } 71 | ``` 72 | 73 | 填充完上面的测试表格,完成测试即可。 74 | 75 | #### 参考资料 76 | 77 | + [TableDrivenTests](https://github.com/golang/go/wiki/TableDrivenTests) 78 | + [关键字驱动测试](http://www.voidcn.com/blog/jaazure/article/p-4333140.html) -------------------------------------------------------------------------------- /tdt/tdt.go: -------------------------------------------------------------------------------- 1 | package tdt 2 | 3 | func Add(a, b int) int { 4 | return a + b 5 | } 6 | 7 | func Min(nums []int) int { 8 | var rst int 9 | if nums != nil { 10 | rst = nums[0] 11 | } else { 12 | return 0 13 | } 14 | for i := range nums { 15 | if rst > nums[i] { 16 | rst = nums[i] 17 | } 18 | } 19 | return rst 20 | } 21 | 22 | func Max(nums []int) int { 23 | var rst int 24 | if nums != nil { 25 | rst = nums[0] 26 | } else { 27 | return 0 28 | } 29 | for i := range nums { 30 | if rst < nums[i] { 31 | rst = nums[i] 32 | } 33 | } 34 | return rst 35 | } 36 | -------------------------------------------------------------------------------- /tdt/tdt_test.go: -------------------------------------------------------------------------------- 1 | package tdt 2 | 3 | import "testing" 4 | 5 | func TestAdd(t *testing.T) { 6 | type args struct { 7 | a int 8 | b int 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | want int 14 | }{ 15 | // TODO: Add test cases. 16 | {"t", args{a: 1, b: 2}, 3}, 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | if got := Add(tt.args.a, tt.args.b); got != tt.want { 21 | t.Errorf("Add() = %v, want %v", got, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func TestMin(t *testing.T) { 28 | type args struct { 29 | nums []int 30 | } 31 | tests := []struct { 32 | name string 33 | args args 34 | want int 35 | }{ 36 | // TODO: Add test cases. 37 | {"t", args{nums: []int{1, 4, 5}}, 1}, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := Min(tt.args.nums); got != tt.want { 42 | t.Errorf("Min() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestMax(t *testing.T) { 49 | type args struct { 50 | nums []int 51 | } 52 | tests := []struct { 53 | name string 54 | args args 55 | want int 56 | }{ 57 | // TODO: Add test cases. 58 | {"t", args{nums: []int{1, 4, 5}}, 5}, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | if got := Max(tt.args.nums); got != tt.want { 63 | t.Errorf("Max() = %v, want %v", got, tt.want) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /testify.md: -------------------------------------------------------------------------------- 1 | ## testify 2 | 3 | testify是一个第三方开发的测试工具包,支持如下特性: 4 | 5 | + [Easy assertions](https://github.com/stretchr/testify#assert-package) 6 | + [Mocking](https://github.com/stretchr/testify#mock-package) 7 | + [HTTP response trapping](https://github.com/stretchr/testify#http-package) 8 | + [Testing suite interfaces and functions](https://github.com/stretchr/testify#suite-package) 9 | 10 | #### 断言(Easy assertions) 11 | 12 | + [github.com/stretchr/testify/assert](github.com/stretchr/testify/assert) 提供了众多的断言函数,帮助我们判断结果是否符合我们预期,如: 13 | 14 | ```go 15 | assert := assert.New(t) 16 | // assert equality 17 | assert.Equal(123, 123, "they should be equal") 18 | 19 | // assert inequality 20 | assert.NotEqual(123, 456, "they should not be equal") 21 | 22 | object := &Object{Value:"Something"} 23 | // assert for nil (good for errors) 24 | assert.Nil(object) 25 | 26 | // assert for not nil (good when you expect something) 27 | if assert.NotNil(object) { 28 | // now we know that object isn't nil, we are safe to make 29 | // further assertions without causing any errors 30 | assert.Equal("Something", object.Value) 31 | } 32 | ``` 33 | 注: assert出错不会中断测试。 34 | 35 | + [testify/require](http://godoc.org/github.com/stretchr/testify/require) 36 | 37 | require包实现了assert包的函数,但是出错的时候不是返回bool类型的数值,而是调用t.FailNow()。 38 | 39 | #### Mocking 40 | 41 | + [testify/mock](http://godoc.org/github.com/stretchr/testify/mock) 42 | + [vektra/mockery](https://github.com/vektra/mockery) 自动生成mock程序 43 | + [mockery-example](https://github.com/jaytaylor/mockery-example) 使用例子 44 | 45 | 创建接口 46 | 47 | ``` 48 | type Stringer interface { 49 | String() string 50 | } 51 | ``` 52 | 53 | 自动生成mock程序 54 | 55 | ```go 56 | mockery -name=Stringer 57 | 58 | // Stringer is an autogenerated mock type for the Stringer type 59 | type Stringer struct { 60 | mock.Mock 61 | } 62 | 63 | // String provides a mock function with given fields: 64 | func (_m *Stringer) String() string { 65 | ret := _m.Called() 66 | 67 | var r0 string 68 | if rf, ok := ret.Get(0).(func() string); ok { 69 | r0 = rf() 70 | } else { 71 | r0 = ret.Get(0).(string) 72 | } 73 | 74 | return r0 75 | } 76 | ``` 77 | 78 | 79 | #### Testing suite interfaces and functions 80 | 81 | ​ 使用[testify/suite](http://godoc.org/github.com/stretchr/testify/suite) 我们可以基于struct构建测试的初始化环境,并且在测试完成之后恢复环境,通过SetupTest和TearDownSuite接口。 82 | 83 | ```go 84 | type ExampleTestSuite struct { 85 | suite.Suite 86 | VariableThatShouldStartAtFive int 87 | } 88 | 89 | // 准备测试环境 90 | func (suite *ExampleTestSuite) SetupTest() { 91 | log.Println("SetupTest") 92 | suite.VariableThatShouldStartAtFive = 5 93 | } 94 | // 恢复测试环境 95 | func (suite *ExampleTestSuite) TearDownTest() { 96 | log.Println("TearDownTest") 97 | } 98 | // 测试案例 99 | func (suite *ExampleTestSuite) TestExample() { 100 | assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) 101 | suite.Equal(5, suite.VariableThatShouldStartAtFive) 102 | } 103 | // 执行ExampleTestSuite结构体里面的全部测试函数 104 | func TestExampleTestSuite(t *testing.T) { 105 | // 运行ExampleTestSuite中Test开始的函数 106 | suite.Run(t, new(ExampleTestSuite)) 107 | } 108 | ``` 109 | 110 | #### 参考资料 111 | 112 | + [go 单元测试进阶篇](https://www.qcloud.com/community/article/921985001483606833) -------------------------------------------------------------------------------- /testify/assert/assert_test.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | type Object struct { 9 | Value string 10 | } 11 | 12 | func TestStopTest(t *testing.T) { 13 | assert :=assert.New(t) 14 | assert.Equal(123, 1123, "they should be equal") 15 | } 16 | 17 | func TestSomething(t *testing.T) { 18 | assert := assert.New(t) 19 | 20 | // assert equality 21 | assert.Equal(123, 123, "they should be equal") 22 | 23 | // assert inequality 24 | assert.NotEqual(123, 456, "they should not be equal") 25 | 26 | 27 | object := &Object{Value:"Something"} 28 | // assert for nil (good for errors) 29 | // assert.Nil(object) 30 | 31 | // assert for not nil (good when you expect something) 32 | if assert.NotNil(object) { 33 | // now we know that object isn't nil, we are safe to make 34 | // further assertions without causing any errors 35 | assert.Equal("Something", object.Value) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /testify/assert/require_test.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/require" 6 | ) 7 | 8 | 9 | func TestRequire(t *testing.T) { 10 | require :=require.New(t) 11 | 12 | // assert equality 13 | require.Equal(123, 1123, "they should be equal") 14 | 15 | // assert inequality 16 | require.NotEqual(123, 456, "they should not be equal") 17 | 18 | } 19 | -------------------------------------------------------------------------------- /testify/mock/mocks/Stringer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0 2 | package mocks 3 | 4 | import mock "github.com/stretchr/testify/mock" 5 | 6 | // Stringer is an autogenerated mock type for the Stringer type 7 | type Stringer struct { 8 | mock.Mock 9 | } 10 | 11 | // String provides a mock function with given fields: 12 | func (_m *Stringer) String() string { 13 | ret := _m.Called() 14 | 15 | var r0 string 16 | if rf, ok := ret.Get(0).(func() string); ok { 17 | r0 = rf() 18 | } else { 19 | r0 = ret.Get(0).(string) 20 | } 21 | 22 | return r0 23 | } 24 | -------------------------------------------------------------------------------- /testify/mock/string.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | 4 | type Stringer interface { 5 | String() string 6 | } 7 | -------------------------------------------------------------------------------- /testify/mock/string_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "testing" 5 | "./mocks" 6 | ) 7 | 8 | func TestString(t *testing.T) { 9 | testObj := new(mocks.Stringer) 10 | // 设置String方法的返回值 11 | testObj.On("String").Return("Hello Mock Testing") 12 | 13 | // assert that the expectations were met 14 | 15 | t.Log(testObj.String()) 16 | testObj.AssertExpectations(t) 17 | } -------------------------------------------------------------------------------- /testify/suite/suite_test.go: -------------------------------------------------------------------------------- 1 | package suite 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type ExampleTestSuite struct { 12 | suite.Suite 13 | VariableThatShouldStartAtFive int 14 | } 15 | 16 | // Make sure that VariableThatShouldStartAtFive is set to five 17 | // before each test 18 | func (suite *ExampleTestSuite) SetupTest() { 19 | log.Println("SetupTest") 20 | suite.VariableThatShouldStartAtFive = 5 21 | } 22 | 23 | func (suite *ExampleTestSuite) TearDownTest() { 24 | log.Println("TearDownTest") 25 | } 26 | 27 | // All methods that begin with "Test" are run as tests within a 28 | // suite. 29 | func (suite *ExampleTestSuite) TestExample() { 30 | log.Println("TestExample") 31 | assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) 32 | suite.Equal(5, suite.VariableThatShouldStartAtFive) 33 | } 34 | 35 | // In order for 'go test' to run this suite, we need to create 36 | // a normal test function and pass our suite to suite.Run 37 | func TestExampleTestSuite(t *testing.T) { 38 | // 运行ExampleTestSuite中Test开始的函数 39 | suite.Run(t, new(ExampleTestSuite)) 40 | } 41 | -------------------------------------------------------------------------------- /word/bench_test.go: -------------------------------------------------------------------------------- 1 | package word 2 | 3 | import "testing" 4 | 5 | func BenchmarkIsPalindrome(b *testing.B) { 6 | for i := 0; i < b.N; i++ { 7 | IsPalindrome("detartrated") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /word/word.go: -------------------------------------------------------------------------------- 1 | // Package word provides utilities for word games. 2 | package word 3 | 4 | // IsPalindrome reports whether s reads the same forward and backward. 5 | // (Our first attempt.) 6 | func IsPalindrome(s string) bool { 7 | for i := range s { 8 | if s[i] != s[len(s)-1-i] { 9 | return false 10 | } 11 | } 12 | return true 13 | } 14 | -------------------------------------------------------------------------------- /word/word_test.go: -------------------------------------------------------------------------------- 1 | package word 2 | 3 | import "testing" 4 | 5 | func TestPalindrome(t *testing.T) { 6 | if !IsPalindrome("detartrated") { 7 | t.Error(`IsPalindrome("detartrated") = false`) 8 | } 9 | if !IsPalindrome("kayak") { 10 | t.Error(`IsPalindrome("kayak") = false`) 11 | } 12 | } 13 | 14 | func TestNonPalindrome(t *testing.T) { 15 | if IsPalindrome("palindrome") { 16 | t.Error(`IsPalindrome("palindrome") = true`) 17 | } 18 | } 19 | 20 | func TestCanalPalindrome(t *testing.T) { 21 | input := "A man, a plan, a canal: Panama" 22 | if !IsPalindrome(input) { 23 | t.Errorf(`IsPalindrome(%q) = false`, input) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /单元测试建议.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feixiao/testing/4c51b33830dd12dc02ed053860d3a6f41b3135de/单元测试建议.pdf --------------------------------------------------------------------------------