Goの基本的なTest手法をFizzBuzzで試してみた

はじめに

今回はGoの標準testing packageを使った基本的なテストの方法を試していきたいと思います。 テストの題材としてFizzBuzz問題を利用してテストを書いていきます。

FizzBuzzの要件

FizzBuzz問題の要件を簡単に整理します。以下の要件を確認するためのテストケースを実装します。

  • 数値を文字列に変換する
  • 3の倍数のときFizzと出力する
  • 5の倍数のときBuzzと出力する
  • 15の倍数のときFizzBuzzと出力する

準備

以下のファイルを作成します。

数値を文字列に変換する実装(サブテスト)

まずは最初の要件として、数値を文字列へ変換するを確認するためのテストを実装します。

import "strconv"

func Stringify(num int) string {
    return strconv.Itoa(num)
}
import "testing"

func Test_Stringify(t *testing.T) {
    if actual, expect := Stringify(1), "1"; actual != expect {
        t.Errorf("actual=%s, expect=%s", actual, expect)
    }
}
> go test
PASS
ok      github.com/y-zumi/tdd   0.005s

これで、数値を文字列へ変換するを実装することができました。では、他の数値でも変換されるかを試してみます。
子テストを追加するためにはサブテストを利用します。サブテストはtesting.T.Runメソッドを使うと書くことができます。

import "testing"

func Test_Stringify(t *testing.T) {
    t.Run("when number is 1", func(t *testing.T) {
        if actual, expect := Stringify(1), "1"; actual != expect {
            t.Errorf("actual=%s, expect=%s", actual, expect)
        }
    })
    t.Run("when number is 2", func(t *testing.T) {
        if actual, expect := Stringify(2), "2"; actual != expect {
            t.Errorf("actual=%s, expect=%s", actual, expect)
        }
    })
    t.Run("when number is 100", func(t *testing.T) {
        if actual, expect := Stringify(100), "100"; actual != expect {
            t.Errorf("actual=%s, expect=%s", actual, expect)
        }
    })
}
> go test -v
=== RUN   Test_Stringify
=== RUN   Test_Stringify/when_number_is_1
=== RUN   Test_Stringify/when_number_is_2
=== RUN   Test_Stringify/when_number_is_100
--- PASS: Test_Stringify (0.00s)
    --- PASS: Test_Stringify/when_number_is_1 (0.00s)
    --- PASS: Test_Stringify/when_number_is_2 (0.00s)
    --- PASS: Test_Stringify/when_number_is_100 (0.00s)
PASS
ok      github.com/y-zumi/tdd   0.005s

サブテストはテストケースに名前をつけることができるため、テストの可読性が高くなります。

3の倍数のときFizzと出力する実装(サブテストによる構造化)

次に3の倍数のときFizzと出力するを実装します。この実装でテストケースが増えるため、各サブテストを数値を文字列へ変換する3の倍数のときFizzと出力するにまとめます。 サブテストを入れ子にするためには、testing.T.Runでサブテスト群をまとめます。

import "strconv"

func Stringify(num int) string {
    if num%3 == 0 {
        return "fizz"
    }

    return strconv.Itoa(num)
}
import "testing"

func Test_Stringify(t *testing.T) {
    t.Run("normal number", func(t *testing.T) {
        t.Run("when number is 1", func(t *testing.T) {
            if actual, expect := Stringify(1), "1"; actual != expect {
                t.Errorf("actual=%s, expect=%s", actual, expect)
            }
        })
        t.Run("when number is 2", func(t *testing.T) {
            if actual, expect := Stringify(2), "2"; actual != expect {
                t.Errorf("actual=%s, expect=%s", actual, expect)
            }
        })
        t.Run("when number is 100", func(t *testing.T) {
            if actual, expect := Stringify(100), "100"; actual != expect {
                t.Errorf("actual=%s, expect=%s", actual, expect)
            }
        })
    })

    t.Run("Multiple of 3", func(t *testing.T) {
        t.Run("when number is 3", func(t *testing.T) {
            if actual, expect := Stringify(3), "fizz"; actual != expect {
                t.Errorf("actual=%s, expect=%s", actual, expect)
            }
        })
    })
}
テスト実行時に階層化されているため、何のテストをしているかが確認しやすくなります。
> go test -v
=== RUN   Test_Stringify
=== RUN   Test_Stringify/normal_number
=== RUN   Test_Stringify/normal_number/number_is_1
=== RUN   Test_Stringify/normal_number/number_is_101
=== RUN   Test_Stringify/Multiple_of_3
=== RUN   Test_Stringify/Multiple_of_3/number_is_3
=== RUN   Test_Stringify/Multiple_of_3/number_is_99
=== RUN   Test_Stringify/Multiple_of_5
=== RUN   Test_Stringify/Multiple_of_5/number_is_5
=== RUN   Test_Stringify/Multiple_of_5/number_is_100
--- PASS: Test_Stringify (0.00s)
    --- PASS: Test_Stringify/normal_number (0.00s)
        --- PASS: Test_Stringify/normal_number/number_is_1 (0.00s)
        --- PASS: Test_Stringify/normal_number/number_is_101 (0.00s)
    --- PASS: Test_Stringify/Multiple_of_3 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_3/number_is_3 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_3/number_is_99 (0.00s)
PASS
ok      github.com/y-zumi/tdd   0.005s

5の倍数のときBuzzを出力する実装(テーブル駆動テスト)

テストケースが多くなってきたのでテーブル駆動テストを使用します。
テーブル駆動テストのメリットは以下のとおりです。

  • インプットとアウトプットの値の見通しが良い
  • テストケースの追加が容易

それではテーブル駆動テストで実装してみましょう。

import "testing"

type TestCase struct {
    description string
    input       int
    output      string
}

func Test_Stringify(t *testing.T) {
    t.Run("normal number", func(t *testing.T) {
        cases := []TestCase{
            {"number is 1", 1, "1"},
            {"number is 101", 101, "101"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })

    t.Run("Multiple of 3", func(t *testing.T) {
        cases := []TestCase{
            {"number is 3", 3, "fizz"},
            {"number is 99", 99, "fizz"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })

    t.Run("Multiple of 5", func(t *testing.T) {
        cases := []TestCase{
            {"number is 5", 5, "buzz"},
            {"number is 100", 100, "buzz"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })
}
=== RUN   Test_Stringify
=== RUN   Test_Stringify/normal_number
=== RUN   Test_Stringify/normal_number/number_is_1
=== RUN   Test_Stringify/normal_number/number_is_101
=== RUN   Test_Stringify/Multiple_of_3
=== RUN   Test_Stringify/Multiple_of_3/number_is_3
=== RUN   Test_Stringify/Multiple_of_3/number_is_99
=== RUN   Test_Stringify/Multiple_of_5
=== RUN   Test_Stringify/Multiple_of_5/number_is_5
=== RUN   Test_Stringify/Multiple_of_5/number_is_100
--- PASS: Test_Stringify (0.00s)
    --- PASS: Test_Stringify/normal_number (0.00s)
        --- PASS: Test_Stringify/normal_number/number_is_1 (0.00s)
        --- PASS: Test_Stringify/normal_number/number_is_101 (0.00s)
    --- PASS: Test_Stringify/Multiple_of_3 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_3/number_is_3 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_3/number_is_99 (0.00s)
    --- PASS: Test_Stringify/Multiple_of_5 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_5/number_is_5 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_5/number_is_100 (0.00s)
PASS
ok      github.com/y-zumi/tdd   0.005s

15の倍数のときFizzBuzzを出力する実装

それでは最後に15の倍数のときFizzBuzzを出力するを実装します。

import "strconv"

func Stringify(num int) string {
    var result string

    if num%3 == 0 {
        result += "Fizz"
    }
    if num%5 == 0 {
        result += "Buzz"
    }
    if result == "" {
        result = strconv.Itoa(num)
    }

    return result
}
import "testing"

type TestCase struct {
    description string
    input       int
    output      string
}

func Test_Stringify(t *testing.T) {
    t.Run("normal number", func(t *testing.T) {
        cases := []TestCase{
            {"number is 1", 1, "1"},
            {"number is 101", 101, "101"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })

    t.Run("Multiple of 3", func(t *testing.T) {
        cases := []TestCase{
            {"number is 3", 3, "Fizz"},
            {"number is 99", 99, "Fizz"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })

    t.Run("Multiple of 5", func(t *testing.T) {
        cases := []TestCase{
            {"number is 5", 5, "Buzz"},
            {"number is 100", 100, "Buzz"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })

    t.Run("Multiple of 15", func(t *testing.T) {
        cases := []TestCase{
            {"number is 15", 15, "FizzBuzz"},
            {"number is 105", 105, "FizzBuzz"},
        }
        for _, c := range cases {
            t.Run(c.description, func(t *testing.T) {
                if actual, expect := Stringify(c.input), c.output; actual != expect {
                    t.Errorf("actual=%s, expect=%s", actual, expect)
                }
            })
        }
    })
}
=== RUN   Test_Stringify
=== RUN   Test_Stringify/normal_number
=== RUN   Test_Stringify/normal_number/number_is_1
=== RUN   Test_Stringify/normal_number/number_is_101
=== RUN   Test_Stringify/Multiple_of_3
=== RUN   Test_Stringify/Multiple_of_3/number_is_3
=== RUN   Test_Stringify/Multiple_of_3/number_is_99
=== RUN   Test_Stringify/Multiple_of_5
=== RUN   Test_Stringify/Multiple_of_5/number_is_5
=== RUN   Test_Stringify/Multiple_of_5/number_is_100
=== RUN   Test_Stringify/Multiple_of_15
=== RUN   Test_Stringify/Multiple_of_15/number_is_15
=== RUN   Test_Stringify/Multiple_of_15/number_is_105
--- PASS: Test_Stringify (0.00s)
    --- PASS: Test_Stringify/normal_number (0.00s)
        --- PASS: Test_Stringify/normal_number/number_is_1 (0.00s)
        --- PASS: Test_Stringify/normal_number/number_is_101 (0.00s)
    --- PASS: Test_Stringify/Multiple_of_3 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_3/number_is_3 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_3/number_is_99 (0.00s)
    --- PASS: Test_Stringify/Multiple_of_5 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_5/number_is_5 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_5/number_is_100 (0.00s)
    --- PASS: Test_Stringify/Multiple_of_15 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_15/number_is_15 (0.00s)
        --- PASS: Test_Stringify/Multiple_of_15/number_is_105 (0.00s)
PASS
ok      github.com/y-zumi/tdd   0.007s

動作確認

これまでテストを実装して動作確認をしました。
最終確認としてmain.goを追加して、最初の要件の通りの挙動になっているか確認します。

  • 数値を文字列に変換する
  • 3の倍数のときFizzと出力する
  • 5の倍数のときBuzzと出力する
  • 15の倍数のときFizzBuzzと出力する
import "fmt"

func main() {
    for i := 1; i <= 110; i++ {
        fmt.Println(Stringify(i))
    }
}
> go run main.go
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
︙
98
Fizz
Buzz
101
Fizz
103
104
FizzBuzz
106
107
Fizz
109
Buzz

期待通りの結果を得ることができました :)

まとめ

本記事ではGoのtestに関する以下の2つの事柄を扱いました。

  • サブテスト
  • テーブル駆動テスト

基本的なことですがGoでテストをするには大事なことなので紹介しました。Goのtestには他にも様々な手法があるのでそのうち紹介できればと思います!

新規機能開発に関する失敗談をLTしてきました!

はじめに

先日、TECH PLAYで開催された若手エンジニアの失敗談LT大会に参加してきました!
本記事ではLTの補足として、開発の失敗原因の一つである 「計画づくり」 に関して深ぼります。

techplay.jp

発表資料

計画づくりとは

計画づくりとはその名の通り、「計画を立てる行為」です。
良い計画づくりをすることには以下の3つの効果があります。

  • プロジェクトに関する潜在リスクの発見
  • 仕様の変更などの開発中の不確実性への対応
  • プロジェクトの実行可否の意思決定

結果として、良い計画づくりを行うとプロジェクトの成功確率は高まります。

悪い計画づくりによる失敗

当時の私の計画づくりはひどいものでした。
フィーチャーの仕様変更や追加がスケジュールに反映することができず、どんどん計画が価値のない物になっていきました。

  • スケジュールにバッファがない
    →実装の遅れが後の作業に影響し、リスケが頻発 😭
  • 見積もりが荒く、不確実性が高い
    →期間内に終わらない量のタスクがそのまま積まれている 😇

これはなんとかせなば!ということで計画づくりを見直すことにしました。

f:id:y-zumi:20190714185950j:plain

計画づくりを改善するために何をしたのか

計画の改善のために、まず、不確実性を反映した計画を作り、そこからフィーチャーの費用対効果を検討し実装する機能を削ることにしました。

不確実性を反映した計画づくり

バッファを考慮した見積もりをするために、今まで理想日による見積もりだけだったのを、理想日とそれに1.5倍掛け合わせた最遅日の2つのスケジュールを立てました。
また、タスクの見積もりは時間ではなく、タスクの複雑さや規模をもとにポイントとして見積もりを行い、労力がかかりすぎないようにしました。

スクラムなどを使っている場合は、プロダクトバックログでスケジュールの変更容易性や、ストーリーポイントによる素早くてざっくりとした見積もりができるので計画づくりには適しているなと感じました。

費用対効果を考慮した実装範囲の再定義

スケジュールの見積もりが完了し、期間内にすべての機能が実装できないとことが明確になりました。 そこで、開発する機能を減らすために「フィーチャーの価値」「見積もった工数をもとに実装の可否と優先順位を決めていきました。

フィーチャーの価値は「利益」「獲得ユーザー数」「課題の解決ができるか」などをもとに定義しました。フィーチャーの価値と前段階で見積もった工数を比較し、費用対効果の高いものを開発スコープ内に含めていくことにしました。
開発の優先順位については「最低限実装すべき機能」→「価値の高いフィーチャー」→「それ以外」という順で並べ替えていきました。

f:id:y-zumi:20190714202559j:plain

まとめ

以上のような不確実性を考慮した計画をもとに実装機能の取捨選択までを、プロジェクト期間中に継続的に行うことで、急な仕様の変更や追加にも対応できるような体制を作ることができました。まだ計画の運用も完璧とはいきませんが、基本的なところは抑えられているのではと思います。計画づくりは奥が深いですが、この経験により計画づくりについて考えなおす良い機会になりました!

今回は失敗LTということで、自分が参加していたプロジェクトの計画づくりに関する失敗を取り上げました。計画づくりはとても難しく、すぐにうまくできるようなものではないと思います。そのため、これからのプロジェクトでも失敗と学びを繰り返して、その気づきをブログ等で発信していけたらと思います!

参考文献

zsh/fishでkubectlの補完をできるようにする

はじめに

最近、仕事でkubernetesを使う機会があったのですが、kubectlの補完ができないことがもどかしかったので設定しました。
zsh/fishの両環境を使っているので、それぞれの設定方法をご紹介します。

zshの設定

zshは公式でcompletionが対応しているので、以下に従って設定をしていきます。
https://kubernetes.io/docs/tasks/tools/install-kubectl/#enabling-shell-autocompletion

~/.zshrcファイルに以下の記述を追加します。

source <(kubectl completion zsh)

zshを再起動するとkubectlの補完ができるようになっているはずです。

fishの設定

fishでは公式のkubectlの補完がサポートされていません。
そのため、こちらに従ってプラグインのインストールを行います。
https://github.com/evanlucas/fish-kubectl-completions

fish plugin managerを使っていない場合

以下のコマンドを入力します

~ ❯❯❯ mkdir -p .config/fish/completions
~ ❯❯❯ cd .config/fish/
~/.config/fish ❯❯❯ git clone https://github.com/evanlucas/fish-kubectl-completions.git

Fisherを使っている場合

以下のコマンドを入力します

~/.config/fish ❯❯❯ fisher add evanlucas/fish-kubectl-completions

利用例

fishの場合、例えば以下のコマンドだとリソースをすべて出してくれるようになります。(AtoZなので、省略コマンドと正式名称のコマンドが並んでいない場合もありますが 😅)

~ ❯❯❯ kubectl explain [tab]
all                         (Resource)  horizontalpodautoscalers  (Resource)  podtemplates            (Resource)
certificatesigningrequests  (Resource)  hpa                       (Resource)  psp                     (Resource)
cj                          (Resource)  ing                       (Resource)  pv                      (Resource)
clusterrolebindings         (Resource)  ingress                   (Resource)  pvc                     (Resource)
clusterroles                (Resource)  ingresses                 (Resource)  quota                   (Resource)
clusters                    (Resource)  job                       (Resource)  rc                      (Resource)
cm                          (Resource)  jobs                      (Resource)  replicasets             (Resource)
componentstatuses           (Resource)  limitranges               (Resource)  replicationcontrollers  (Resource)
configmap                   (Resource)  limits                    (Resource)  resourcequotas          (Resource)
configmaps                  (Resource)  namespace                 (Resource)  rolebindings            (Resource)
controllerrevisions         (Resource)  namespaces                (Resource)  roles                   (Resource)
crd                         (Resource)  netpol                    (Resource)  rs                      (Resource)
crds                        (Resource)  networkpolicies           (Resource)  sa                      (Resource)
cronjobs                    (Resource)  no                        (Resource)  sc                      (Resource)
cs                          (Resource)  node                      (Resource)  secret                  (Resource)
csr                         (Resource)  nodes                     (Resource)  secrets                 (Resource)
customresourcedefinition    (Resource)  ns                        (Resource)  service                 (Resource)
daemonsets                  (Resource)  pdb                       (Resource)  serviceaccounts         (Resource)
deploy                      (Resource)  persistentvolumeclaims    (Resource)  services                (Resource)
deployment                  (Resource)  persistentvolumes         (Resource)  statefulsets            (Resource)
deployments                 (Resource)  po                        (Resource)  storageclass            (Resource)
ds                          (Resource)  pod                       (Resource)  storageclasses          (Resource)
endpoints                   (Resource)  poddisruptionbudgets      (Resource)  sts                     (Resource)
ep                          (Resource)  podpreset                 (Resource)  svc                     (Resource)
ev                          (Resource)  pods                      (Resource)
events                      (Resource)  podsecuritypolicies       (Resource)

まとめ

今回はzsh/fishでのkubectl commandの補完機能を紹介しました。
補完機能で快適なkubectlライフを!