BDDフレームワークのgodogでブラウザで動く振る舞いのテストを書く

こんにちは！むらってぃーです。
[Go3 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go3)の4日目を担当させていただきます。

今回はGoで開発されているBDDフレームワークを使い、ブラウザで動くテストを書いていきます。
結合テストの1つとして、プロダクトに役立ちそうであれば是非参考にしていただけると嬉しいです。

## BDDとは
まず、BDDとは「ビヘイビア駆動開発」を指します。
ビヘイビア駆動開発の概要は下記です。

> BDDではスペック（仕様）とテストは限りなく近い物である。従って、テスト駆動開発における「テストファースト」は、BDDにおいては「スペックファースト」となり、スペックを作ってから実装するという、より自然な形でのプログラム製作を実現している。

> いくつかのテストフレームワークは、

> アプリケーションの振る舞いを記述するストーリーフレームワーク
オブジェクトの振る舞いを記述するスペックフレームワーク
の2種類を含む。

出典: [Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%93%E3%83%98%E3%82%A4%E3%83%93%E3%82%A2%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA)

つまりは、テストを書いてから実装するテスト駆動開発と少し異なり、仕様を書いてからその振る舞いを満たすプログラムを書く形です。

## BDDフレームワーク
BDDを行うためにいくつかフレームワークが用意されています。
代表的なものとしていくつか例を上げます。

* JBehave
  * Javaのテスティングフレームワーク
* xSpec
  * Rubyの「RSpec」を始祖とするテスティングフレームワークの総称
* Cucumber
  * 「振る舞いを、フォーマットがある自然言語で書くfeatureファイル」と「実際のテスト実行を、プログラミング言語で書くstepファイル」の2つで1つのテストを構成
  * さまざまなプログラミング言語でその派生が開発されている

参考リンク: [TDD／BDDの思想とテスティングフレームワークの関係を整理しよう](https://www.atmarkit.co.jp/ait/articles/1403/25/news033.html)

## godogとは
![スクリーンショット 2020-11-23 13.42.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/da2966e0-8981-3226-44f5-3cc4960dedff.png)

BDDフレームワークであるCucumberをGoで扱う派生ライブラリです。
このライブラリを使うと、Cucumberの形式である「featureで振る舞いを書き、stepでテスト実行をGoで書く」形式でテストをかけます。

https://github.com/cucumber/godog

ホットドッグをイメージしたものなのでしょうか。Gopherがパンに挟まれてる姿が可愛いです。

## godogを使ってテストを書く
テストの仕様として使う題材は以前書いた記事である、[Cucumber × Puppeteer × chai でBDD開発におけるE2Eテスト実行環境の構築](https://qiita.com/Canon11/items/fe33d79333edad84d16f) と同じ物を使います。

内容は下記の通りです。

* DockerのNGINXイメージを使う
* シナリオ
  * シナリオ名: nginxの初期表示画面から公式ページに飛ぶことができる
  * 前提条件: nginxの初期表示画面が表示されている
  * アクション: nginx.com のリンクをクリックする
  * 結果: 遷移したページに Welcome to NGINX! が表示されている。

ではいきましょう。

### godogコマンドインストール

```
$ go get -u github.com/cucumber/godog/cmd/godog
```

go getで入れたコマンドへのパスを通しておく必要があります。
下準備はこれだけです。

### nginxコンテナ立ち上げ
とりあえずdockerでサクッと。

```
$ docker run -p 8080:80 nginx
```

### featureファイル用意
スペックを書くためのファイルを用意します。
godogでは、featuresディレクトリの中に置かれたスペックのファイルを自動で読み取ってくれます。
しかし、いきなりプロジェクトルートにfeaturesがあると何のこっちゃになるので、e2eというディレクトリを切ってその中に入れます。

```e2e/features/nginx_scenario.feature
Feature: nginx画面
  エンドユーザーがnginxの様々なページで動作を行うシナリオ

  Scenario: nginxの初期画面から公式ページに飛ぶことができる
    Given nginxの初期画面が表示されている
    When "nginx.com" のリンクをクリックする
    Then 遷移したページに "Welcome to NGINX!" が表示されている
```

### テストを動かす
e2eディレクトリに移動してgodogコマンドを打つと、テスト実行結果が出力されます。

```
$ cd e2e; godog
Feature: nginx画面
  エンドユーザーがnginxの様々なページで動作を行うシナリオ

  Scenario: nginxの初期画面から公式ページに飛ぶことができる         # features/nginx_scenraio.feature:4
    Given nginxの初期画面が表示されている
    When "nginx.com" のリンクをクリックする
    Then 遷移したページに "Welcome to NGINX!" が表示されている

1 scenarios (1 undefined)
3 steps (3 undefined)
647.359µs

You can implement step definitions for undefined steps with these snippets:

func StepDefinitioninition1(arg1 string) error {
	return godog.ErrPending
}

func StepDefinitioninition2(arg1 string) error {
	return godog.ErrPending
}

func nginx() error {
	return godog.ErrPending
}

func FeatureContext(s *godog.Suite) {
	s.Step(`^"([^"]*)" のリンクをクリックする$`, StepDefinitioninition1)
	s.Step(`^遷移したページに "([^"]*)" が表示されている$`, StepDefinitioninition2)
	s.Step(`^nginxの初期画面が表示されている$`, nginx)
}
```

3つのスペックに対するテストの実装がされていないという出力です。
この出力の下半分がsnippetsになっていて、これをコピペするだけでテストファイルができます。

では、これをコピペしてテストファイルを作りましょう。

```e2e/nginx_scenario_test.go
package e2e

import "github.com/cucumber/godog"

func StepDefinitioninition1(arg1 string) error {
	return godog.ErrPending
}

func StepDefinitioninition2(arg1 string) error {
	return godog.ErrPending
}

func nginx() error {
	return godog.ErrPending
}

func FeatureContext(s *godog.Suite) {
	s.Step(`^"([^"]*)" のリンクをクリックする$`, StepDefinitioninition1)
	s.Step(`^遷移したページに "([^"]*)" が表示されている$`, StepDefinitioninition2)
	s.Step(`^nginxの初期画面が表示されている$`, nginx)
}
```

この状態で再度テストを動かします。すると、

```
❯ godog
Feature: nginx画面
  エンドユーザーがnginxの様々なページで動作を行うシナリオ

  Scenario: nginxの初期画面から公式ページに飛ぶことができる         # features/nginx_scenario.feature:4
    Given nginxの初期画面が表示されている                   # nginx_scenario_test.go:14 -> nginx
      TODO: write pending definition
    When "nginx.com" のリンクをクリックする               # nginx_scenario_test.go:6 -> StepDefinitioninition1
    Then 遷移したページに "Welcome to NGINX!" が表示されている # nginx_scenario_test.go:10 -> StepDefinitioninition2

1 scenarios (1 pending)
3 steps (1 pending, 2 skipped)
266.809µs
```

このように、1つ目のSpecでPendingで止まっているのがわかります。

テストが動いているのが確認できたので、いよいよ中身を書いていきます。

### 中身を書く
今回は [agouti](https://github.com/sclevine/agouti) というライブラリを使って、chromedriver経由でブラウザを操作します。

テストの中身はこのようになりました。

```e2e/nginx_scenario_text.go
package e2e

import (
	"errors"
	"fmt"
	"time"

	"github.com/cucumber/godog"
	"github.com/sclevine/agouti"
)

var globalPage *agouti.Page
var globalDriver *agouti.WebDriver

func SeeNginxWelcomeView() error {
	// ブラウザでnginxのwelcomeページにアクセス
	if err := globalPage.Navigate("http://localhost:8080"); err != nil {
		return err
	}

	h1Text, err := globalPage.Find("h1").Text()
	if err != nil {
		return err
	}

	if h1Text != "Welcome to nginx!" {
		return errors.New("nginx初期画面ではありません")
	}
	return nil
}

func ClickLink(text string) error {
	// text が書かれているリンクをクリックする
	err := globalPage.FirstByLink(text).Click()
	if err != nil {
		return err
	}

	// 遷移時間分待つ(本当はsleep使わないで頑張りたい)
	time.Sleep(1 * time.Second)
	return nil
}

func SeeH1(wantText string) error {
	h1Text, err := globalPage.Find("h1").Text()
	if err != nil {
		return err
	}

	if h1Text != wantText {
		return fmt.Errorf("%s は h1 要素として見つかりません", wantText)
	}
	return nil
}

func FeatureContext(s *godog.Suite) {
	// テストシナリオの前処理でセッションを用意する
	s.BeforeSuite(func() {
		globalDriver = agouti.ChromeDriver(agouti.Browser("chrome"))
		if err := globalDriver.Start(); err != nil {
			panic(err)
		}

		page, err := globalDriver.NewPage()
		if err != nil {
			panic(err)
		}
		globalPage = page
	})
	
	// テストシナリオの後処理でWebdriverを止める
	s.AfterSuite(func() {
		globalDriver.Stop()
	})
	s.Step(`^nginxの初期画面が表示されている$`, SeeNginxWelcomeView)
	s.Step(`^"([^"]*)" のリンクをクリックする$`, ClickLink)
	s.Step(`^遷移したページに "([^"]*)" が表示されている$`, SeeH1)
}
```

先ほどのシナリオに対して、それぞれのブラウザで行う操作を書いています。

この状態でgodogを動かすと下記のように出力されます。

```
❯ godog
Feature: nginx画面
  エンドユーザーがnginxの様々なページで動作を行うシナリオ

  Scenario: nginxの初期画面から公式ページに飛ぶことができる         # features/nginx_scenario.feature:4
    Given nginxの初期画面が表示されている                   # nginx_scenario_test.go:15 -> SeeNginxWelcomeView
    When "nginx.com" のリンクをクリックする               # nginx_scenario_test.go:32 -> ClickLink
    Then 遷移したページに "Welcome to NGINX!" が表示されている # nginx_scenario_test.go:42 -> SeeH1

1 scenarios (1 passed)
3 steps (3 passed)
9.789966057s
```

全てのStepがPassし、シナリオもPass状態となりました。
これで、godogを使ってテストを用意し、実行することができました。

## 最後に
今回はBDDフレームワークのgodogを使ってテストを書きました。
ブラウザを使ったテストだけではなく、APIやgRPCの結合テストにも使用することができます。
スペックベースでテストを書けば、featureファイル自体が仕様を表すものにもなってきます。
そのため、チームに参画する新規メンバーのプロダクトへのキャッチアップにも使用することが可能です。

是非参考にしてみてください。