JMDC TECH BLOG

JMDCのエンジニアブログです

Ginkgoを使ってGoをテストしてみた

こんにちは。プロダクト開発部の西原です。

現在、JMDCが保有しているヘルスビッグデータを活用して生活者や医療に新しい価値を提供する新規プロダクト開発チームのバックエンドを担当しております。

今年、JMDCではアドベントカレンダーに参加しています。 qiita.com

本記事は、JMDC Advent Calendar 2023 18日目の記事です。

開発を進めていく中でどうテストを行っていくか、どのテストフレームワークを使用するか検討することは、よくあるのではないでしょうか。

新規プロダクトのバックエンドはGo言語で開発しており、今回”Ginkgo”と呼ばれるテストフレームワークを導入しました。個人的に使いやすかったので、ぜひ共有させていただければと存じます。

Ginkgoとは

GinkgoとはGo言語のビヘイビア駆動開発用(BDD)のテストフレームワークです。

また、Ginkgoは、GomegaというMatcherと組み合わせて使用します。

基本的に振る舞い(要求仕様)を元にテストコードを作成するため、複雑なテストを記述しやすいメリットがあります。今回は実際にGinkgoを導入して簡単なテストを実行するところまでご紹介します。

インストール

Ginkgo は go モジュールを使用します。go.modファイル設定があると仮定し、以下をインストールします。

go get github.com/onsi/ginkgo/ginkgo

go get github.com/onsi/gomega/...

後述しますが、インストール後はginkgo コマンドを使ってテストが可能になります。

テスト

ginkgo をインストールしたら、実際にテストファイルを作成して、テストを実行するところまで実践してみたいと思います。

① テストスイートファイルの作成

テスト対象のディレクトリ内でginkgo bootstrap コマンドを実行すると作成されます。

今回は、例としてentity ディレクトリを作成します。

$ mkdir entity && cd entity

$ ginkgo bootstrap

# Generating ginkgo test suite bootstrap for Person in:

#   entity_suite_test.go

上記のコマンドで下記のファイルが作成されました。

package entity

import (
    "testing"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
)

func TestEntity(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Entity Suite")
}

② テストファイルの作成

Ginkgoのテストスペック作成機能を利用してテストスペックのテンプレートを作成します。

(コマンドを使わず、直接ファイルを作成することも可能です)

今回はentityディレクトリの中にuser.goを作成します。

$ ginkgo generate user

上記のコマンドで下記のファイルが作成されました。

package entity_test

import (
    "github.com/path/to/your/project/entity"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
)

var _ = Describe("User", func() {

})

こちらの var _ = Describe("User", func() { })の中でテストを実装していきます。

今回は簡単にUserの以下の要件に対してテストケースを作成すると仮定します。

  • フルネームを取得する
  • 年齢が成人かどうか
package entity

import (
    "/path/to/your/project/entity"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
)

var _ = Describe("User", func() {
    var user *entity.User

    // BeforeEach(): テストケースが実行される前に共通のセットアップ
    BeforeEach(func() {
        user = &entity.User{
            FirstName: "John",
            LastName:  "Doe",
            Age:       18,
        }
    })

    // When(): 特定の状態や条件に基づいてテストケースをまとめるために使用
    When("フルネームを取得するとき", func() {
        // It(): 期待される振る舞いや結果を定義
        It("正常に取得できる", func() {
            Expect(user.GetFullName()).To(Equal("John Doe"))
        })
    })

    When("ユーザーが成人かどうか判定するとき", func() {
        // Context(): When()と同様の役割
        // 私は When() の中でさらに特定の条件が何かを記述するときに使用してます
        Context("年齢が 18 歳の場合", func() {
            It("成人である", func() { Expect(user.IsAdult()).To(BeTrue()) })
        })

        Context("年齢が 17 歳の場合", func() {
            It("成人でない", func() {
                user.Age = 17
                Expect(user.IsAdult()).To(BeFalse())
            })
        })
    })
})

user.goは以下のように実装します。

package entity

import "fmt"

type User struct {
    FirstName string
    LastName  string
    Age       int
}

func (u *User) GetFullName() string {
    return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
}

func (u *User) IsAdult() bool {
    const adultAge = 18
    return u.Age >= adultAge
}

③ テストの実行

ginkgoコマンドで実行します。実行した結果は以下です。

$ ginkgo -v -cover
Running Suite: Entity Suite - /path/to/your/project/entity
========================================================
Random Seed: 1701522124

Will run 8 of 8 specs
------------------------------
User when フルネームを取得するとき 正常に取得できる
/path/to/your/project/entity/user_test.go:21
• [0.000 seconds]
------------------------------
User when ユーザーが成人かどうか判定するとき 年齢が 18 歳の場合 成人である
/path/to/your/project/entity/user_test.go:28
• [0.000 seconds]
------------------------------
User when ユーザーが成人かどうか判定するとき 年齢が 17 歳の場合 成人でない
/path/to/your/project/entity/user_test.go:33
• [0.000 seconds]
------------------------------

Ran 3 of 3 Specs in 0.005 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
        /path/to/your/project/entity   coverage: 100.0% of statements
composite coverage: 100.0% of statements

Ginkgo ran 1 suite in 823.179917ms
Test Suite Passed

今回使用したginkgoコマンドのオプションは以下です。

  • -v: 実行するスペックとそのスペックのファイル行番号を表示します
  • -cover: カバレッジ情報を表示します。このフラグが有効になると、カバレッジの割合が表示されます

テスト失敗時

テスト時にエラーが発生した場合は、エラーの詳細がわかりやすく出力されます。

※ 下記は意図的に user.go で adultAge = 17に変更して、エラーを発生させた場合です。

// ... 先略

------------------------------
User when ユーザーが成人かどうか判定するとき 年齢が 17 歳の場合 成人でない
/path/to/your/project/entity/user_test.go:33
  [FAILED] in [It] - /path/to/your/project/entity/user_test.go:33
• [FAILED] [0.001 seconds]
User when ユーザーが成人かどうか判定するとき 年齢が 17 歳の場合 [It] 成人でない
/path/to/your/project/entity/user_test.go:33

  [FAILED] Expected
      <bool>: true
  to be false
  In [It] at: /path/to/your/project/entity/user_test.go:33
------------------------------

// ... 後略

実際のプロダクト開発で行ったこと

今回はあくまでチュートリアルのご紹介でしたが、実際のプロダクトでは以下のようにGinkgoを導入していました。

  • Dockerコンテナ上でGinkgoを導入
  • GitHub Actionsを使ってCI/CD上でGinkgoを実行
  • PR作成時にCI/CD上で実行されたGinkgoのカバレッジ率を可視化
  • モックと組み合わせてテストケースを実装

上記のように活用することで複雑なテストケースを作成でき、プロダクションコードも正常な動作を担保できました。

このあたりの詳細は、また次回ブログにさせていただければと存じます。

おわりに

今回はGinkgoを使って、Goをテストする方法をご紹介しました。

紹介した内容は簡単なテストケースでしたが、プロダクションコードでも振る舞いベースに作成することで実装漏れやバグの減少といった効果を実感できました。Goでテストをしたい方がいらっしゃいましたら、本ブログが少しでも参考になれば嬉しいです。

明日19日目は、同じバックエンド開発の吉川さんです!お楽しみに♪

JMDCでは、ヘルスケア領域の課題解決に一緒に取り組んでいただける方を積極採用中です!フロントエンド /バックエンド/ データベースエンジニア等、様々なポジションで募集をしています。詳細は下記の募集一覧からご確認ください。 hrmos.co

まずはカジュアルにJMDCメンバーと話してみたい/経験が活かせそうなポジションの話を聞いてみたい等ございましたら、下記よりエントリーいただけますと幸いです。 hrmos.co

★最新記事のお知らせはぜひ X(Twitter)をご覧ください! https://twitter.com/jmdc_tech