LoginSignup
17
20

More than 5 years have passed since last update.

Ginkgoでgolangのビヘイビア駆動(BDD)開発入門

Posted at

TL;DR

  • ginkgo(Go言語のビヘイビア駆動開発用のテストフレームワーク)で,golangのテストを実行する方法を紹介します
  • 以下の内容について記載しています
    • ginkgo, gomegaのインストール
    • チュートリアル
      • テストの雛形の作成手順
      • 実装
      • テストの実行(Pass時,Fail時)

Ginkgo概要

  • Ginkgoは,Go言語のビヘイビア駆動開発用のテストフレームワークです
    • 複雑なテストを記述しやすい,と言っています
    • Ginkgoは,GomegaというMather(テストの条件マッチ)と組み合わせて使います
    • assertはカッコよくないらしいので,Ginkgoなどでコードの振る舞いをベースにテストを記述します

GinkgoとGomegaのインストール

  • Ginkgoの案内にしたがって,ライブラリをインストールします
    • ginkgoはコマンドの形で提供されます
    • go testの代わりに実行します
    • それ以外にも,テスト用のテンプレートを作る機能もあります
# 環境変数GOPATHで指定した場所のbinディレクトリにginkgoがインストールされます
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...

which ginkgo
~/.go/bin/ginkgo

チュートリアル

テスト付きのパッケージ作成

  • テスト付きパッケージは,以下の手順で作ります

    • 既存のパッケージファイルにginkgo用のテストファイルを設置することもできますが,テストファーストの考え方から,テストから作ります
    • パッケージのテストスイートファイルを作る
    • パッケージファイルごとのテストファイルを作る
  • テストスイートファイル,テストファイルは,作成のためのコマンドが提供されています

    • パッケージを格納するディレクトリ(この記事の例ではPerson)を作成して,ginkgo bootstrapコマンドを実行すると,Personパッケージ用のテストスイートファイルPerson_suite_test.goが作成されます
    • Personパッケージのテストを行うための共通設定です
# Aliceパッケージを作るディレクトリを作成する
mkdir Person && cd Person

# テストスイートファイルの作成
ginkgo bootstrap
# Generating ginkgo test suite bootstrap for Person in:
#   Person_suite_test.go

# テストファイルの作成
ginkgo generate Person
# Generating ginkgo test for Person in:
#   Person_test.go
  • テストスイート,テストファイルを作っただけでは,パッケージのテストができませんので,パッケージ本体(この記事ではPerson)を作ります

パッケージPersonの確認

  • 以下のようなパッケージPersonを作ることを考えます
    • 構造体Personを持つ
    • 構造体Personは,string型のフィールドnameを持つ
    • フィールドnameに関するSetterとGetterである以下のメソッドを持つ
      • SetName(string): フィールドnameをセットする
      • GetName() (string, error): nameを読み出す.nameがセットされていない場合はエラー"Name is not set"が返る
  • テストファーストの考え方から,テストからまず作ります

Person_test.goの実装

  • ginkgo generateで作られたPerson_test.goは雛形で,中身がありません
  • そのため,前節のPersonの仕様を満たすようにテストを記述します
    • 2つのメソッドに対し,3つのテストを行っています
      • SetNameするだけのテスト
      • SetNameしたnameがGetNameで取り出せることを確認する
      • GetNameでエラーが返る(nameをセットする前にGetNameする)パターンを確認する
    • Ginkgoの書き方をしています.Gomegaで変数のマッチ処理を行います
/*
Test file for Person
Person_test.go
*/
package Person_test

import (
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"

    "errors"
    . "." // Person.goと同じディレクトリで実行するので,カレントディレクトリをimportします
)

var _ = Describe("Person", func() { // Personに関するテストです.文字列は変更できます
    Context("Test for Name", func() { // Nameに関するテストです.文字列は変更できます
        /* Person.SetNameメソッドのテスト */
        It("Test for SetName", func() {
            p := &Person{} // 構造体Personを作ります
            // エラーは発生しないので特段の検証は必要でない
            p.SetName("Alice") // SetNameメソッドを実行します
        })
        /* Person.GetNameメソッドのテスト(正常系) */
        It("Test for GetName (Normal)", func() {
            p := &Person{}
            p.SetName("Bob") // SetNameメソッドでnameにBobをセットします
            name, err := p.GetName() // GetNameメソッドでnameを読み出します
            Expect(err).To(BeNil())  // エラーは発生しない(nil)ことが期待されます
            Expect(name).To(Equal("Bob")) // nameはBobだと期待されます
        })
        /* Person.GetNameメソッドのテスト(エラー系) */
        It("Test for GetName (Error)", func() {
            p := &Person{}
            _, err := p.GetName() // エラーなはずなので,nameは使用しません
            // nameが設定されていないので,エラーが返る
            Expect(err).NotTo(BeNil()) // エラーが発生します(err != nil)
            Expect(err).To(Equal(errors.New("Name is not set"))) // エラー内容が正しいか確認します
        })
    })
})
  • Describe, Context, Itの順で,階層的なブロックに分けてテストを記述します
  • 各ブロックの中で,テストを記述します
    • この記事の場合は,構造体Personとその関連メソッドSetName, GetNameに関するテストです
  • メソッドを実行して,返り値として期待される値を,Expectメソッドを使って記述します
    • Expectメソッドの引数に判定したい変数を指定します
    • 続いて,True(To)またはFalse(NotTo)を判定するメソッドを設定します
    • ToNotToメソッドの引数として,値を設定します

Person.goの実装

  • 仕様に基づいて,Person.goを実装します
/*
Person.go
*/
package Person

import "errors"

type Person struct{
    name string
}

func (p *Person) GetName() (string, error) {
    if p.name == "" {
        return "", errors.New("Name is not set")
    }
    return p.name, nil
}

func (p *Person) SetName(in_name string) {
    p.name = in_name
}

ginkgoでテストの実行(すべてのテストがパスするとき)

  • Person_test.goPerson.goが実装できたので,テストを実行します
    • Person_suite_test.goはこの記事の段階で編集する必要はありません
  • テストの実行は,ginkgoコマンドまたはgo testコマンドです
    • 以下の例では,カバレッジレポートも作っています
# ginkgoの代わりにgo testコマンドでも良い
ginkgo -v -cover -coverprofile=./cover.out
Running Suite: Person Suite
===========================
Random Seed: 1557386542
Will run 3 of 3 specs

Person Test for Name
  Test for SetName
  /Person/Person_test.go:14
•
------------------------------
Person Test for Name
  Test for GetName (Normal)
  Person/Person_test.go:20
•
------------------------------
Person Test for Name
  Test for GetName (Error)
  /Person/Person_test.go:28
•
Ran 3 of 3 Specs in 0.000 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
coverage: 100.0% of statements

Ginkgo ran 1 suite in 1.159211242s
Test Suite Passed
  • 各テストが通過した場合は,PASSが表示されます

ginkgoでテストの実行(テストの一部がパスしなかったとき)

  • もしもテストに失敗した場合は,失敗した部分でその旨が出力されます
    • 以下の正常系GetNameのテストで,errにnilでない値が入っていると記述した場合は,テストがパスしません
// Person.go
func (p *Person) GetName() (string, error) {
    if p.name == "" {
        return "", errors.New("Name is not set")
    }
    return p.name, errors.New("No Error") // 返り値にnilでない値を設定している
    // return p.name, nil
}
  • ginkgoでテストを実行すると,以下のようにFAILしたテストと関連情報が出力されます
$ ginkgo -v -cover -coverprofile=./cover.out
Running Suite: Person Suite
===========================
Random Seed: 1557387339
Will run 3 of 3 specs

Person Test for Name
  Test for SetName
  /Person/Person_test.go:14
•
------------------------------
Person Test for Name
  Test for GetName (Normal)
  /Person/Person_test.go:20

• Failure [0.000 seconds]
Person
/Person/Person_test.go:11
  Test for Name
  /Person/Person_test.go:12
    Test for GetName (Normal) [It]
    /Person/Person_test.go:20

    Expected
        <*errors.errorString | 0xc0001b6150>: {s: "No Error"}
    to be nil

    /Person/Person_test.go:24
------------------------------
Person Test for Name
  Test for GetName (Error)
  /Person/Person_test.go:28
•

Summarizing 1 Failure:

[Fail] Person Test for Name [It] Test for GetName (Normal)
/Person/Person_test.go:24

Ran 3 of 3 Specs in 0.001 seconds
FAIL! -- 2 Passed | 1 Failed | 0 Pending | 0 Skipped
--- FAIL: TestPerson (0.00s)
FAIL
coverage: 100.0% of statements

Ginkgo ran 1 suite in 1.2348396s
Test Suite Failed
  • Expectで記述した期待の変数値と,実際の変数値が表示されているので,デバッグに利用できます

まとめ

  • ginkgoフレームワークを用いると,「振る舞い(関数の入出力関係)」ベースでテストを記述することができます
  • プログラムコードもテストに合わせて実行されるので,きれいなコードになる傾向があります
  • 今後の展開
    • モックとの組み合わせ
    • ランダムな数値を入力とする場合の対応

参考

17
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
20