猫も杓子も結合テスト

はじめまして、LOBでGopherをやっているChifung Cheungです。

今回はSSPの開発で広告配信の結合テストの設計と実施についての話を軽くしたいと思います。

# 結合テストの設計

## なぜ結合テスト

アドテクの世界では、SSP(Supply Side Platform)がビジネスモデルの起点だと言っても過言ではありません。
SSPの安心安全な広告配信があるこそ、DMPやDSPなどのプレヤーの仕事が成り立ちます。
なのでSSPの安定性は、クライアントと我らの利益と直結する重要視すべきファクターであります。
その安定性を保証するに最重要のは、やはりテストです。

## そもそも結合テストとは?

人によって結合テストの定義がかなりばらつきがありますが、
個人的にはGoogle Testing Blogが提唱するTest sizesが一番わかりやすいと思います。
Googleはテストの実行時間や影響範囲に基づき、テストを主にSmall, Medium, Largeの三種類に分類しました。

| Feature | Small | Medium | Large |
|:--|:--|:--|:--|
| Network access | No | localhost only | Yes |
| Database | No | Yes | Yes |
| File system access | No | Yes | Yes |
| Use external systems | No | Discouraged | Yes |
| Multiple threads | No | Yes | Yes |
| Sleep statements | No | Yes | Yes |
| System properties | No | Yes | Yes |
| Time limit (seconds) | 60 | 300 | 900+ |

出典：https://testing.googleblog.com/2010/12/test-sizes.html

- Smallテスト
  - 一つのコンポーネントに限るテスト
  - DBやネットワークはモックで
  - 実行時間が短い
- Mediumテスト
  - 複数のコンポーネントの結合のテスト
  - DBやネットワークは使用可能
  - できる限り高速で
- Largeテスト
  - 更に広い範囲のコンポーネントを網羅できる
  - 本番に近い
  - 実行時間が長い

Smallテストの定義がユニットテストに近い、Golangの`go test`を利用するだけで十分ですので、
今回は割愛します。
MediumテストとLargeテストは一般的な結合テストの定義に近いが、どちらを選ぶのが問題です。

## SSP広告配信における適切なテスト範囲を決める

SSP広告配信のテストケースの特徴というと

- ビジネスモデル自体が簡単
- 依存するDSPやメディアなどの外部システムが種類も数多い
- 配信の安定性が収益と強く直結している

Largeテストを実施できるならもちろんありがたいですが、
工数の制限や影響範囲が広いなどの問題があるのでなかなか進まないです。
そこで妥協しMediumテストだけを行うのがやむを得ない選択となります。

また、そもそもLargeテストの必要性自体も怪しい、
メディアやDSP(Demand Side Platform)の個別な仕様によって発生する多くのバグは本番環境でしか再現しないバグです、
Largeテストをするよりに、**リアルの環境ではCanary Releaseを行うほうがコスパがよいです**。

したがって、**今回のテストはMediumテストに限定しました**。

具体的には、配信システムを立ち上げて、ブラウザを起動して、ダミーメディアにアクセスして、
広告が正しく表示されるか、ログが正常に記録されるかのテストとなります。

MediumテストがSmallテストより実行時間が長いでカバレッジも正しく計測できないので、
コンポーネントの結合部分に着眼したテストだけをMediumテスト範囲としました。

## プラットフォーム非依存性(Platform Agnostic)

LOBのSSPがGKE上で動いていますが、
今回のテストの目的はプラットフォーム（実行環境）のテストではありません。
GKEであろうかなかろうか全く関係なしでk8sでも開発者のローカル環境でも動くのは一番望ましいです。

そこにDockerComposeを使うのもありですが、ローカルのテストのしやすさも考慮すると、
mattnさん謹製のプロセス管理ツールのgoremanを選びました。
https://github.com/mattn/goreman

(自分も昔似たものを作ったが、まだ絶賛開発中ので導入するのをやめました https://github.com/cheung-chifung/gompose)

## Monorepo

世の中に、サービスごとに複数のリポジトリで管理するmanyrepoと、
すべてのサービスを一つのリポジトリで管理するmonorepoという２つのソース管理の方法論があります。

monorepoが一見クレイジーなやり方ですが、導入してみると手応えが良かったです。
今までmanyrepoのほうが多かったが、近年にmonorepoを採用するチームがかなり増えてきました。
実用主義者のGopherたちの中に人気が高いだけではなく、他にフロントエンド界隈のBabelなどもmonorepoを使っています。
https://github.com/babel/babel/blob/master/doc/design/monorepo.md

manyrepoだとプラットフォームを構築するのに複数の依存環境をクローンして設定＆ビルドをしなければならないが、
LOBのSSPはmonorepoを使っていてワンクリックで環境を起動できるので助かりました。

## 結合テストのアーキテクチャ

こちらは今回考えたアーキテクチャの簡素版です。

![Screen Shot 2018-12-05 at 11.59.56.png](https://qiita-image-store.s3.amazonaws.com/0/86204/e358486f-5854-1527-b323-0ad0687c8c2a.png)

- **SSP**: 本番ですでに稼働している既存なシステム
- **SSP-Debugger**: AdTechにおけるSSP以外の登場人物
  - **Dummy DSP**: 入札するDSPたち
  - **Dummy Site**: 広告タグを貼り付けるダミーメディア
  - **Platform Debugger**: プラットフォームが依存するデータベースなどのデータを操るためのデバッガーサービス
- **SSP-Tester**: 結合テストの部品たち
  - **TestSuites**: テストケース集
  - **SSP-TestKit**: SSP-DebuggerとSSPを操るライブラリ
  - **TestAgent**: テストケースを実行するためのアプリケーション
  - **ChromeHeadless**, **BrowserStack**: 広告を閲覧するためのブラウザ

SSP-DebuggerとSSPの各サービスにはhookというgRPCのサービスがあって、
キャッシュクリアやログ収集などのテストするためのインターフェイスを全部gRPCとして定義されています。
SSP-TestKitにはこれらのhookをgRPC経由で接続して、各部品たちの挙動を操りながらテストを行います。

# 結合テストの実施

## テストケースの例

上記の部品が揃ったらこのようなテストを書くのが可能となります。
例えば一つの広告枠から２つのDSPに繋ぐ場合：

```golang
t := testkit.NewAgent()
t.Add("Banner ad display, 2-bidders").
	AddSetups(
		setups.NewSetupAd(DummyAdSet...),
		setups.NewSetupDsps(bidder1, bidder2),
		setups.NewSetupChromeHeadlessBrowser().WithTimeout(30*time.Second),
	).
	AddTests(
		tests.NewTestBannerAdDisplay("DUMMY_AD_ID").
			WithUserID("DUMMY_USER_ID").
			WithClick("DUMMY_AD_ID").
			AdSetting_ShouldBe("DUMMY_AD_ID", "300", "250"),
		tests.NewTestBidResponse(2).
			WinnerSeatID_ShouldBe("DSP-1").
			Price_ShouldBe(13.0),
		tests.NewTestImpLog("DUMMY_AD_ID", "DSP-1"),
	)
t.Run()
```

`Setup`と`Test`は下記のようなgoのinterface、

```golang
type Setup interface {
	Name() string
	Setup(*testkit.Agent) error
}

type Test interface {
	Name() string
	Run(*testkit.Agent) error
}
```

サイトの閲覧やログの確認などの`Test`を予め定義しておけば、テストケースも簡単に書けます。

## Test workflowに統合

既存のワークフローに結合テストを追加することで、全部のテストをCircleCI上に実施できることになりました。
現状の結合テストはまだ実行時間が短いのでコミットする際に毎回動かすのは可能ですが、実行時間が長くなると分散化したり実行頻度を下げたりしなければなりません。
もう一つ要注意するのは、CircleCIのdocker executorは複数プロセスの間のコネクションをテストできないので、machine executorを選ばなければなりません。

![Screen Shot 2018-12-05 at 13.01.34.png](https://qiita-image-store.s3.amazonaws.com/0/86204/44503d95-21dd-958e-0674-61c4cf7019de.png)

# 成果と課題

結合テストを導入してからデグレを数回検知できました、
手動でテストしても多分発見できそうなバグがほとんどが、今までより高速でPDCAを回せてすごくありがたいです。
特に大きな改修をする際に、結合テストがあれば多少安心しました。

ただし、結合テストが通っても必ずバグしないの保証がありません。
結合テストがカバーできるスコップがまだ狭いで、BrowserStackによって各種のブラウザを使ったテストとかはまだ開発途中です。
Canavy Releaseができない限り、手動テストは結局避けられませんでした。

また、Mediumテストが増えると、テストがSmallでやるべきかMediumでやるべきかの問題が発生します。
LOBにはTest Engineer職がいないので、すべてのエンジニアにMediumテストを書いていただくのが難しいです。
個人的にはできる限りSmallテストを書いて頂いて、あとからMediumテストに詳しいエンジニアが結合テストを足すのが無難だと思いますが、まだ模索中です。

課題はまだ多いが、優秀な仲間と一緒に改善できればと思います！

では、次回のLOBのアドベントカレンダーをお楽しみください！