Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Go言語でTDDする勉強会 第2回(2019/12/04) 発表メモ

Go言語でTDDする勉強会 第2回(2019/12/04) 発表メモ

connpass

Go言語でTDDする勉強会 (初心者大歓迎!)

Slack

Join Weeyble on Slack. ( weeyble.slack.com )
事前に登録をお願いします

channel: #go

前提

ローカル環境でGo言語が動作すること

教材

https://github.com/quii/learn-go-with-tests

参加者への質問

  • 前回(2019/11/20) 未参加で、今回が初参加の方はいますか?
    • ローカル環境でGo言語が動作する状態ですか?
    • Go言語を動かしたことはありますか?
  • Go言語を実務で経験している方はいますか?
    • むしろ教えて下さい...

本勉強会の管理者

今回の発表者


教材の説明

https://github.com/quii/learn-go-with-tests

  • Table of contentsGo fundamentals の欄
    • 1. Install Go から 17. Maths まで
      • 前回(2019/11/20) は 2. Hello, world まで終了
  • さらに その下の Build an application の欄
    • HTTP server など、application を作成しながら TDD を学ぶ構成
  • 各章毎に、解説とソースコードが並ぶ構成
  • 全ての章の ページ先頭 に、ソースファイルが格納されているディレクトリへの リンク(さらにその直下に v1, v2, etc...)
    • 基本的に解説内にソースコードも記述されていますが、たまに情報不足の時があるのでその時はファイルを確認して下さい

例: 2. Hello, worldFrench

    if language == french {
        return frenchHelloPrefix + name
    }

定数: french, frenchHelloPrefix の定義部分が載っていない

v6 のソースファイル で確認

// 前略
const french = "French"
// 中略
const frenchHelloPrefix = "Bonjour, "
// 以下略

2. Hello, world の軽い復習

2. Hello, World

  • it's up to you how you structure your folders. (フォルダをどのように構成するかはあなた次第です)
  • Hello, World
    • 定番 "Hello, world" から始まって (hello.go)
    • go run hello.go 実行
  • How to test
    • separate your "domain" code from the outside world (side-effects) ("domain" code を 外の世界(side effect(副作用))から分離する)
      • fmt.Printlnside effect(副作用)
        • stdout(標準出力) に出力するため
      • 戻り値で string を返す関数を作成 -> Hello()
    • test code (hello_test.go) 追加
    • go test 実行
  • Writing tests
    • ファイルの名: xxx_test.go
    • 関数名 = Test から始まる
    • 関数の引数 = t *testing.T
  • if
    • 補足: A Tour of Go: If 参照
      • > 括弧 ( ) は不要で、中括弧 { } は必要です
  • Declaring variables
    • :=
      • 補足: A Tour of Go: Short variable declarations 参照
        • > 関数の中では、 var 宣言の代わりに、短い := の代入文を使い、暗黙的な型宣言ができます
        • > 関数の外では、キーワードではじまる宣言( var, func, など)が必要で、 := での暗黙的な宣言は利用できません
  • t.Errorf
    • print out a message and fail the test (メッセージを print して、テストに失敗します)
    • 補足: testing パッケージ - golang.jp 参照
      • > Errorfは、Logf()とFail()を呼び出すことと同じです
        • > Logfは、Printf()と同じように、引数をそれぞれに指定した書式で書式化し、テキストをエラーログに記録します
        • > Failは、テスト関数でエラーがあったが実行は継続したことを記録します
    • %q
      • wraps your values in double quotes (値を""で囲みます)
  • Go doc
  • Hello, YOU
    • next requirement: 引数追加
    • test code を先に書いて
      • go test
        • コンパイルエラー
    • コンパイルエラー 解消のための 最小限の実装
      • go test
        • 期待値が異なるので テスト失敗
    • test pass するための正しい実装
      • go test -> test pass
  • Constants (定数化)
    • const
    • 補足: A Tour of Go: Constants 参照
      • > 定数は、文字(character)、文字列(string)、boolean、数値(numeric)のみで使えます
      • > 定数は := を使って宣言できません
  • Hello, world... again
    • t.Run
      • subtests
      • test を group 化
      • describing different scenarios (異なるシナリオを記述します)
    • リファクタリング: assertion 部分を 関数 として抽出
      • In Go you can declare functions inside other functions and assign them to variables (Goでは、他の関数内で関数を宣言して変数に割り当てることができます)
    • t.Helper()
      • テスト失敗時に報告される 行数 が helper 関数 内ではなく、 helper 関数を呼び出している側の 行数 となる
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)
        }
    }
    // 以下略
}
  • Discipline
  • Keep going! More requirements
    • 多言語対応. 第2引数追加
  • switch
  • one...last...refactor?
    • リファクタリング: switch で言語判定している部分を 関数 として抽出
    • named return valuenaked return を使用
      • 補足: A Tour of Go: Basics - Named return values 参照
        • > この戻り値の名前は、戻り値の意味を示す名前とすることで、関数のドキュメントとして表現するようにしましょう
        • > return ステートメントに何も書かずに戻すことができます。これを "naked" return と呼びます
        • > naked returnステートメントは、短い関数でのみ利用すべきです
func greetingPrefix(language string) (prefix string) {
    switch language {
    case french:
        prefix = frenchHelloPrefix
    // 中略
    }
    return
}

named return value -> prefix
naked return -> return 単独. ( naked return を利用しない書き方=return prefix も可能 )


3. Integers

3. Integers

  • Integers
    • integers ディレクリ作成
      • ディレクトリ移動 cd ../integers
    • adder_test.go 作成
    • Go source files can only have one package per directory, (Go ソースファイルには、ディレクトリごとに 1つの package しか含めることができません。)
    • -> Five suggestions for setting up a Go project | Dave Cheney ※後述
  • Write the test first
    • %d
      • 10進数表記
    • note that we are no longer using the main package (main package を使用していないことに注意)
      • instead we've defined a package named integers, (代わりに integers という名前の package を定義)
  • Try and run the test
    • go test 実行
    • コンパイルエラー
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • コンパイルを通すための(しかし test は失敗する)コードを記述
    • (x, y int)
    • go test
      • adder_test.go:10: expected '4' but got '0'
        • 期待値が異なるので テスト失敗
        • the test is correctly reporting what is wrong. (test は何が間違っているかを正しく報告)
    • named return value
      • aren't using the same here. (ここでは同じものを使用していません)
      • It should generally be used when the meaning of the result isn't clear from context, (戻り値 の意味が context から明らかにならない時に使用すべき)
      • in our case it's pretty much clear that Add function will add the parameters (この場合、Add関数が引数を追加することはかなり明らかです)
    • -> CodeReviewComments · golang/go Wiki ※後述
  • Write enough code to make it pass
    • go test -> test pass
  • v1 終了
  • Refactor
    • 関数にコメント追加
    • Go Doc で表示される
  • Examples
func ExampleAdd() {
    sum := Add(1, 5)
    fmt.Println(sum)
    // Output: 6
}
  • v2 終了

以下 3. Integers から リンクされていた記事の紹介

Five suggestions for setting up a Go project | Dave Cheney

Five suggestions for setting up a Go project | Dave Cheney

Creating a package

  • The name of the package should match the name of the directory (package の名前は、ディレクトリの名前と一致 する必要があります)
    • 補足: A Tour of Go: Packages 参照
      • > パッケージ名はインポートパスの最後の要素と同じ名前になります
      • > インポートパスが "math/rand" のパッケージは、 package rand ステートメントで始まるファイル群で構成します
      • > (もしURLを含むインポートパスが "golang.org/x/net/websocket" だった場合は、 package websocket になります)
  • Package names should be all lower case. (package 名はすべて小文字)
  • should contain only letters, numbers (文字、数字のみ)
    • absolutely no punctuation. (句読点は絶対に含めない)
  • The name of a package is part of the name of every type, constant, variable, or function (package 名は、すべての型、定数、変数、または関数の名前の一部です)
    • Avoid repetition. (繰り返しを避ける)
      • () bytes.BytesBuffer, strings.StringReader
      • () bytes.Buffer, strings.Reader
  • For more advice on naming -> What's in a name? ※後述
  • All the files in a package’s directory must have the same package declaration, (パッケージのディレクトリ内の すべてのファイルは同じ package 宣言 を持つ必要があります)
  • you can’t put the code for multiple packages into one directory. (複数の package のコードを1つのディレクトリに配置できません)

Main packages

  • Main packages deviate from the previous statement about the package declaration and the packages’ directory being the same. (main package は「package 宣言と package のディレクトリが同じであるという前の文」からは外れます)
  • the name of the command is taken from the name of the package’s directory. (command の名前 は package の ディレクトリの名前から取得 されます)
  • This obviates the need to use flags like -o when building (これにより build 時に -o などのフラグを使用する必要がなくなります)

例) hello ディレクトリ 直下に main 関数 を定義している hello.go が存在する状態で go build を実行

% pwd
/<中略>/learn-go-with-tests/hello

% ls
hello.go  hello_test.go

% cat hello.go
package main
(※中略)
func main() {
        fmt.Println(Hello("world", ""))
}

% go build

実行可能ファイル hello が作成されている -> ./hello で実行

% ls -F
hello*  hello.go  hello_test.go

% ./hello
Hello, world

hello -> helloooo にディレクトリの名前を変更 してから再び go build を実行

% cd ..
% mv hello helloooo
% cd helloooo

% go build

実行可能ファイル helloooo が作成されている -> ./helloooo で実行

% ls -F
hello*  hello.go  hello_test.go  helloooo*

% ./helloooo
Hello, world

Everything in Go works with packages.

  • The go commands ... all work with packages (goコマンドはすべてパッケージで動作します)
    • go build
    • go install
    • go test
    • go get
  • go run is the exception to this rule (go run は この規則の例外です)

The import path

  • The import path is effectively the full path to your package. (import パスは、実質的に package へのフルパス です)
  • Note: There is no concept of sub packages in Go. (注: Go には sub package の概念はありません)

VCS names in import paths

  • In Go, the convention is to include the location of the source code (Goでは、ソースコードの場所を含めるのが慣例です)
    • github.com/golang/glog

Sample repositories


以下 Five suggestions for setting up a Go project | Dave Cheney から リンクされていた記事の紹介

What's in a name?

What's in a name?

  • p5. MixedCase
    • MixedCase を使う
    • アンダースコア(_)区切りは
    • acronym(頭字語. URL, HTTP, ID など) は大文字のみ
  • p6 〜 p8. local 変数
    • local 変数 は短く!!
    • i, r, b の方が良い
      • index, reader, buffer よりも
    • 例) RuneCount という名前の関数内で
      • count の方が良い
        • runeCount よりも
      • 関数名から runecount なのは明らか
  • p9. 引数
    • 引数 も local 変数 と同様
    • しかし Go Doc で表示される
    • 型 が descriptive(説明的な) な場合は 変数名 を 短くすべき
      • 例) Duration の 変数名 = d
        • duration ではなく
        • 型から「期間」なのが明確
    • 型 が int64, []byte など ambiguous(曖昧)な場合は 変数名 を documentation として提供
      • 例) func HasPrefix(s, prefix []byte) bool
        • 第2引数 が prefix
        • 第1引数 の s が 検索対象の文字列なのが分かる
  • p10. 戻り値
    • 先頭大文字の関数(exported function) の 戻り値
      • ドキュメント化のためにのみ、名前を付ける必要
  • p11 Receiver の 変数名
    • receiver の 型の名前 の 先頭1文字 or 先頭2文字
  • p13 Interface の 型名
    • メソッド名 + er
      • Read() を定義している Interface -> Reader
    • 英語として間違っていても、そのまま er を付けて良い
      • Exec() を定義している Interface -> Execer
        • 辞書で execer を検索しても出てこない
    • 例外: ReadByte() を定義している Interface -> ByteReader
      • ReadByte が逆
    • 複数のメソッド を定義している Interface の場合
      • choose a name that accurately describes its purpose (目的を正確に説明する名前を選択してください)
      • 例) ResponseWriter, ReadWriter

以下 3. Integers から リンクされていた記事の紹介

Named Result Parameters - CodeReviewComments · golang/go Wiki

Named Result Parameters - CodeReviewComments · golang/go Wiki

  • func (n *Node) Parent1() *Node (名前無し) の方が良い
    • func (n *Node) Parent1() (node *Node) よりも
    • Node という型名で分かるし、戻り値が1個だけだし
  • if a function returns two or three parameters of the same type, or if the meaning of a result isn't clear from context, adding names may be useful in some contexts. (関数が同じ型の2つまたは3つのパラメーターを返す場合、または結果の意味がコンテキストから明確でない場合、名前を追加することが一部のコンテキストで役立つ場合があります)
    • -> func (f *Foo) Location() (lat, long float64, err error) の方が clear
      • func (f *Foo) Location() (float64, float64, error) よりも
      • latitude (緯度), longitude (経度) を返す事が明確
  • Don't name result parameters just to avoid declaring a var inside the function; (関数内でvarを宣言しないようにするためだけに、結果パラメーターに名前を付けないでください。)
    • 楽をするため(変数宣言を省略)だけに使ってはいけない
  • Naked returns are okay if the function is a handful of lines. (関数が少数の行であれば、 Naked returns は問題ありません)
  • in some cases you need to name a result parameter in order to change it in a deferred closure (場合によっては、遅延クロージャーで変更するために結果パラメーターに名前を付ける必要があります)


4. Iteration

4. Iteration

  • Iteration
  • Write the test first
    • iteration ディレクリ作成
      • ディレクトリ移動 cd ../iteration
    • repeat_test.go 作成
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • repeat.go 作成
  • Write enough code to make it pass
  • v1 終了
  • Refactor
    • +=
    • repeatCount を const 化
  • v2 終了
  • Benchmarking
    • func BenchmarkXxx(*testing.B)repeat_test.go に追記
      • b.N
        • the framework will determine what is a "good" value (framework は "good" value を決定します)
    • go test -bench=. 実行
  • vx 終了

Practice exercises

exercises (1) Change the test so a caller can specify how many times the character is repeated (テストを変更して、呼び出し元が文字の繰り返し回数を指定できるように)

  • Try and run the test
    • test code (repeat_test.go) から修正
    • Repeat() の 第2引数 を追加
    • 後の step で テスト失敗 させたいので 期待値(expected) も変更
 func TestRepeat(t *testing.T) {
-   repeated := Repeat("a")
-   expected := "aaaaa"
+   repeated := Repeat("a", 6)
+   expected := "aaaaaa"
 func BenchmarkRepeat(b *testing.B) {
    for i := 0; i < b.N; i++ {
-       Repeat("a")
+       Repeat("a", 6)
  • go test
    • コンパイルエラー
% go test

./repeat_test.go:6:20: too many arguments in call to Repeat
    have (string, number)
    want (string)
./repeat_test.go:16:9: too many arguments in call to Repeat
    have (string, number)
    want (string)
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • repeat.go 修正
    • Repeat() の 第2引数 を追加
-func Repeat(character string) string {
+func Repeat(character string, count int) string {
  • go test
    • テスト失敗
%  go test
--- FAIL: TestRepeat (0.00s)
    repeat_test.go:10: expected "aaaaaa" but got "aaaaa"
  • Write enough code to make it pass
    • repeat.go 修正
-const repeatCount = 5
-
 func Repeat(character string, count int) string {
    var repeated string
-   for i := 0; i < repeatCount; i++ {
+   for i := 0; i < count; i++ {
        repeated += character
    }
  • go test
% go test
PASS

exercises (2) Write ExampleRepeat

  • Try and run the test
    • test code (repeat_test.go) に追記
    • 3. Integers からコピペ
func ExampleAdd() {
	sum := Add(1, 5)
	fmt.Println(sum)
	// Output: 6
}

  • Write enough code to make it pass
func ExampleRepeat() {
	repeated := Repeat("a", 6)
	fmt.Println(repeated)
	// Output: aaaaaa
}
  • go test -v

exercises (3) Have a look through the strings package

  • Find functions you think could be useful and experiment with them by writing tests like we have here. (役に立つと思われる関数を見つけ、ここにあるようなテストを書いて試してください)
  • Try and run the test
    • test code (repeat_test.go) から修正
    • TestRepeat2() 新規追加
func TestRepeat2(t *testing.T) {
	repeated := Repeat2("a", 6)
	expected := "aaaaaa"

	if repeated != expected {
		t.Errorf("expected %q but got %q", expected, repeated)
	}
}
  • go test
    • コンパイルエラー
./repeat_test.go:30:14: undefined: Repeat2
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • repeat.go 追記
    • Repeat2() 新規追加
func Repeat2(character string, count int) string {
	return ""
}
  • go test
    • テスト失敗
%  go test
--- FAIL: TestRepeat2 (0.00s)
    repeat_test.go:34: expected "aaaaaa" but got ""
+import "strings"
+
 func Repeat2(character string, count int) string {
-   return ""
+   return strings.Repeat(character, count)
  • go test
% go test
PASS

5. Arrays and slices

5. Arrays and slices

  • Write the test first
    • arrays ディレクトリ作成
      • ディレクトリ移動 cd ../arrays
    • sum_test.go 作成
    • array の書き方 2種類
      • [N]type{value1, value2, ..., valueN}
      • [...]type{value1, value2, ..., valueN}
    • %v
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
  • Write enough code to make it pass
  • v1 終了
  • Refactor
    • range を使用して array をループ
      • 1個目の戻り値: index
      • 2個目の戻り値: value
      • 補足: A Tour of Go: Range 参照
        • > rangeは反復毎に2つの変数を返します
        • > 1つ目の変数はインデックス( index )で
        • > 2つ目はインデックスの場所の要素のコピーです
    • _
      • index は使用しないので、1個目の戻り値を無視
      • 補足: A Tour of Go: Range continued 参照
        • > インデックスや値は、 " _ "(アンダーバー) へ代入することで捨てることができます
        • > もしインデックスだけが必要なのであれば、 " , value " を省略します
  • v2 終了
  • Arrays and their type
    • An interesting property of arrays is that the size is encoded in its type. (配列の興味深い特性は size が その型の中に エンコードされる ことです)
  • Write the test first
    • use the slice type
      • array myArray := [3]int{1,2,3} ではなく
      • slice mySlice := []int{1,2,3} の テストケース 追加
  • Write the minimal amount of code ... (test を実行するための最小限のコードを記述し、失敗する test の出力を確認)
    • 対応策は 以下のいずれか
      • (1) Break the existing API by changing the argument to Sum to be a slice rather than an array (Sum の引数を arrayではなく slice に変更して、既存のAPIを破壊します)
      • (2) Create a new function
    • 今回は前者の「既存のAPIを破壊」を選択
      • In our case, no-one else is using our function so rather than having two functions to maintain let's just have one. (この場合、他の誰も関数を使用していないので、2つの関数を維持するのではなく、1つだけにしましょう)
    • Sum(numbers [5]int) -> Sum(numbers []int) に変更
      • 1個目の test ( t.Run("collection of 5 numbers", … ) が コンパイルエラー
        • numbers := [5]int{1, 2, 3, 4, 5} -> numbers := []int{1, 2, 3, 4, 5} に変更
  • Write enough code to make it pass
  • v3 終了
  • Refactor
    • Every test has a cost
    • In our case, you can see that having two tests for this function is redundant. (この場合、この関数に対して2つのテストを行うことは冗長であることがわかります)
    • go test -cover 実行
      • coverage: 100.0%
    • delete one of the tests and check the coverage again. (テストの1つを削除し、カバレッジを再度確認)
      • 1個目の test ( t.Run("collection of 5 numbers", … ) を削除
      • coverage: 100.0% で変わらない
    • next challenge -> SumAll 新規関数

※今回はここまで

次回 -> 第3回 2019/12/18 実施です (第3回 発表メモ)


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.