├── LICENSE.md ├── Makefile ├── README.md ├── architecture.png ├── article.md ├── circleci.png ├── fig.pptx ├── sample-nginx ├── Dockerfile ├── entrypoint └── nginx.conf.tmpl └── test ├── go.mod ├── mock_ap.go ├── nginx.go ├── nginx_test.go └── run /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Cybozu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t sample-nginx:latest sample-nginx 3 | 4 | test: build 5 | test/run 6 | 7 | .PHONY: build 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # test-nginx-with-go-and-docker 2 | 3 | これは、nginx を Go と Docker で自動テストするサンプルです。 4 | 5 | - `sample-nginx/` 6 | - テスト対象となる nginx の設定ファイルや Dockerfile です。 7 | - `test/` 8 | - nginx をテストするための Go プログラムやテストの起動スクリプトです。 9 | 10 | ## テストの実行方法 11 | 12 | テストを実行するには、リポジトリのトップで `make test` を実行してください。 13 | テストの実行には docker が必要です。 14 | 15 | ## テストコードの説明 16 | 17 | - `nginx_test.go` 18 | - テストケースが記述されているコードです。 19 | - `nginx.go` 20 | - テスト対象となる nginx を起動したり終了したりするコードです。内部で docker コマンドを叩いています。 21 | - `mock_ap.go` 22 | - APサーバーのモックを起動したり終了したりするコードです。 23 | - `run` 24 | - テスト実行を開始するためのシェルスクリプトです。 25 | 26 | ## テストを実行する環境 27 | 28 | ローカル環境でも CircleCI 環境でも実行できるようにするために、テストは以下のような構成になっています。 29 | 30 | ![architecture](./architecture.png) 31 | 32 | 太い青枠で囲われた部分が Docker コンテナです。 33 | 34 | テストは nginx-tester というコンテナの中で実行されます。 35 | それぞれのテストはテスト対象となる nginx コンテナを起動します。 36 | 37 | nginx-tester と nginx が相互に通信できるようにするために、nginx コンテナは nginx-tester と同じ docker network 内に配置します。 38 | この docker network は `run` というシェルスクリプトがテストの起動前に作成します。 39 | 40 | AP のモックサーバーは独立したコンテナではなく、nginx-tester 内の goroutine として起動します。 41 | 42 | テストケースは並列に実行されるので、nginx のコンテナや MockAP は複数個同時に実行されます。nginx のコンテナに suffix が付けられているのはそのためです。 43 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybozu/SAMPLE-test-nginx-with-go-and-docker/1add1870e5621631e33bff33f0089e27ffc46f7e/architecture.png -------------------------------------------------------------------------------- /article.md: -------------------------------------------------------------------------------- 1 | # 複雑怪奇な nginx を Go と Docker でユニットテストする 2 | 3 | 全国の nginx 職人のみなさま、こんにちは。野島([@nojima](https://twitter.com/nojima))です。 4 | 5 | この記事では nginx のユニットテスト[^1]に関する知見を紹介したいと思います。 6 | 7 | [^1]: ここでは nginx をひとつのユニットとみなしています(ユニットテストにおける「ユニット」が何を指すかは定義によって異なっており、統一されていません。この記事では nginx がひとつのユニットとなるような定義を採用したと解釈してください)。 8 | 9 | ## 背景 10 | 11 | nginx は極めて柔軟なロードバランサであり、プロダクション環境ではその柔軟さを生かして多彩な役割を担っています。 12 | 我々の nginx は、ユーザーからのリクエストを AP サーバーに振り分け、アクセス制限を行い、リクエストをリダイレクトし、HTTPヘッダを付与したり削ったりしています。 13 | しかし、nginx は便利な反面、その設定は極めて複雑になり、読解したり変更したりするのが難しくなっています。 14 | そこで、**nginx をユニットテストする仕組み** を Go と Docker で作りました。 15 | 16 | もともと nginx の設定を動作確認するには、データセンターにデプロイするしかありませんでした。 17 | デプロイには結構時間がかかるので、動作確認はとても面倒でした。 18 | 19 | ユニットテストの導入により動作確認は非常に効率化されました。 20 | ユニットテストは他のサーバーに依存していないので、ローカル環境やCI環境でも実行できます。 21 | よって、データセンターにデプロイすることなく、手元でサクッと動作を確認できます。 22 | また、トピックブランチを CI 環境で常にテストしておくこともできます。 23 | 24 | テストはすべて Docker の内側で行われるので、ローカル環境に特殊なセットアップをしておく必要はありません。 25 | Docker がインストールされていれば動きます。 26 | 27 | この記事では、このユニットテストの仕組みを説明していきたいと思います。 28 | 29 | ## サンプルコード 30 | 31 | この記事のために説明用のサンプルコードを用意しました。 32 | ローカルに clone してきて `make test` するとテストを実行できます。 33 | 34 | https://github.com/cybozu/SAMPLE-test-nginx-with-go-and-docker 35 | 36 | ## テスト対象の nginx 37 | 38 | サンプルコードでは、テスト対象となる nginx は以下のような設定になっています(一部抜粋)。 39 | APサーバーにリバプロするエンドポイントや、常に 400 を返すエンドポイントなどがあります。 40 | 41 | ```nginx 42 | server { 43 | listen 80; 44 | 45 | location / { 46 | proxy_pass http://${AP_SERVER_ADDR}; 47 | ... 48 | } 49 | 50 | location /secret/ { 51 | deny all; 52 | } 53 | 54 | ... 55 | } 56 | ``` 57 | 58 | 注目してほしいのは、`proxy_pass` の部分です。 59 | APサーバーのアドレスを直接設定ファイルに埋め込むのではなく、`AP_SERVER_ADDR` という環境変数に切り出しています。 60 | これは、テストする際にAPサーバーをモックサーバーに置き換える必要があるためです。 61 | 62 | 切り出した環境変数は、コンテナ起動時に [envsubst][envsubst] で具体的な値に展開します。 63 | サンプルコードでは [entrypoint というシェルスクリプト](https://github.com/cybozu/SAMPLE-test-nginx-with-go-and-docker/blob/master/sample-nginx/entrypoint)がその作業をやっています。 64 | 65 | サンプルコードでは環境変数は `AP_SERVER_ADDR` のみでしたが、他にも環境によって値が変わる設定項目があればすべて環境変数に切り出す必要があります。 66 | 例えば、`resolver` を使っている場合、テスト環境では Docker の DNS サーバーである `127.0.0.11` を指定しないといけないので、環境変数に切り出します。 67 | 68 | [envsubst]: https://www.gnu.org/software/gettext/manual/gettext.html#envsubst-Invocation 69 | 70 | ## テストの概観 71 | 72 | テストコードの説明に入る前に、テストの概観を図を使って説明します。 73 | ローカル環境でも CircleCI 環境でも実行できるようにするために、テストは以下のような構成になっています。 74 | 75 | ![architecture](./architecture.png) 76 | 77 | 太い青枠で囲われた部分が Docker コンテナを表しています。nginx-tester と nginx という2種類のコンテナがあります。 78 | 79 | - `nginx-tester` 80 | - `go test -v ./...` を実行するコンテナです。 81 | - テストの実行には Go と docker が必要なので、nginx-tester のイメージには `circleci/golang:1.14` を使っています。 82 | - nginx-tester は必要に応じて nginx コンテナを起動します。 83 | - コンテナの中から別のコンテナを起動するために **Docker outside of Docker** の構成を取っています。つまり、ホストの `/var/run/docker.sock` をコンテナ内にマウントすることで、コンテナからホストの docker を操作できるようにしています。 84 | 85 | - `nginx-xxxxxx` 86 | - テスト対象となる nginx を格納しているコンテナです。 87 | - コンテナ名の `-xxxxxx` の部分は実際にはランダムな文字列です。これは、同時に複数個起動したときに名前が被るのを防ぐためです。 88 | 89 | nginx-tester と nginx が相互に通信できるようにするために、nginx コンテナと nginx-tester は同じ docker network に所属しています。 90 | この docker network はテストの起動前にシェルスクリプトで作成しておきます。 91 | 92 | AP のモックサーバーは独立したコンテナではなく、nginx-tester 内の goroutine として起動します。図の破線で囲われた部分が goroutine を表しています。 93 | 94 | 次に CircleCI 上でどのようにテストを実行するかを説明します。 95 | CircleCI では、`setup_remote_docker` を使って作成されたリモート環境で docker が実行されます。 96 | テストは、このリモート環境の中で実行されます。 97 | 98 | ![circleci](./circleci.png) 99 | 100 | CircleCI でこのテストを走らせるにあたって、注意しないといけないのは次の2点です。 101 | 102 | 1. リモート環境と Primary Container (`config.yml` に書かれたステップを実行するコンテナ)の間では、セキュリティ上の理由から、HTTP や TCP などの通信が行えません。よって、リモート環境との通信は `docker` コマンドを用いたものに限定されます。 103 | 2. リモート環境では、Primary Container 上のファイルシステムが見れません。つまり、リモート環境からはソースコードが見えないということです。 104 | 105 | 実は、nginx-tester をローカルで直接実行せずにわざわざコンテナ内で実行していたのは、1 の問題を回避するためでした。 106 | 107 | 2 の問題に関しては [CircleCI の公式ドキュメントに回避策が載っています](https://circleci.com/docs/2.0/building-docker-images/#mounting-folders)。 108 | 以下の手順を行うことで、Primary Container から `nginx-tester` にソースコードを渡せます。 109 | 110 | 1. `nginx-test-data` という、空のボリュームを持つダミーのコンテナを作成する。 111 | 2. `docker cp` でソースコードをダミーコンテナに転送する。 112 | 3. `docker run` の `--volumes-from` オプションを使って、ダミーコンテナのボリュームをマウントする。 113 | 114 | さて、テストの概観がわかったところで実際のテストコードを見ていきましょう。 115 | まずはより簡単なリバプロなしの場合から始めます。 116 | 117 | ## テストコード (リバプロなし) 118 | 119 | 次のテストは `GET /secret/` で 400 が返ってくることを確認するものです。 120 | 121 | ```go 122 | func TestSecretEndpoints(t *testing.T) { 123 | t.Parallel() 124 | 125 | nginx := StartNginx(t, NginxConfig{}) // ① 126 | defer nginx.Close(t) 127 | nginx.Wait(t) 128 | 129 | resp, err := http.Get(nginx.URL() + "/secret/") // ② 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | defer resp.Body.Close() 134 | 135 | if resp.StatusCode != http.StatusForbidden { // ③ 136 | t.Errorf("status code should be 400, but %d", resp.StatusCode) 137 | } 138 | } 139 | ``` 140 | 141 | ① `StartNginx()` は nginx コンテナを起動する関数です。詳細は後述します。 142 | 次に `nginx.Wait()` で起動が完了するまで待ちます。 143 | 144 | ② nginx に対して `GET /secret/` を行います。`http.Get()` は単なる Go の標準関数です。 145 | 146 | ③ レスポンスを assert します。これも普通の Go のテストコードです。 147 | 148 | 前節では Docker を使ってテストすると説明しましたが、Docker はこのテストケースのコードからは完全に隠蔽されています。 149 | これは、Docker にまつわる複雑さをテストケースから分離することで、テストケースを読みやすくするためです。 150 | 151 | テストコードでは関心の分離や単一責任の原則が蔑ろにされることがよくありますが、私はテストコードでもこれらの原則は重要であると考えています。 152 | 153 | Docker を実際に操作しているのは `StartNginx()` や `nginx.Close()` などの関数です。 154 | それでは `StartNginx()` の実装を見ていきましょう。 155 | 156 | ## StartNginx() 157 | 158 | `StartNginx()` の肝となる部分は以下のコードです。この関数の主な仕事は docker コマンドを叩くことです。 159 | 160 | ```go 161 | // docker コマンドを叩いて sample-nginx:latest を起動する。 162 | args := []string{ 163 | "run", "--rm", 164 | "--name", name, 165 | "--net", network, 166 | "-e", fmt.Sprintf("AP_SERVER_ADDR=%s", config.APServerAddress), 167 | "sample-nginx:latest", 168 | } 169 | cmd := exec.Command("docker", args...) 170 | if err := cmd.Start(); err != nil { 171 | t.Fatal(err) 172 | } 173 | ``` 174 | 175 | このコードで注目してほしい点は、`-e` で `AP_SERVER_ADDR` に値を渡していることです。 176 | これにより、任意のAPサーバーを差し込んで nginx を起動できるわけです。 177 | それでは、実際にAPサーバーを差し込むテストを見ていきましょう。 178 | 179 | ## テストコード (リバプロあり) 180 | 181 | 次のテストは AP サーバーをモックしてリバプロの挙動を確認するものです。 182 | 183 | ```go 184 | func TestReverseProxy(t *testing.T) { 185 | t.Parallel() 186 | 187 | ap := StartMockAP(t) // ① 188 | defer ap.Close(t) 189 | 190 | nginx := StartNginx(t, NginxConfig{ // ② 191 | APServerAddress: ap.Address(), 192 | }) 193 | defer nginx.Close(t) 194 | nginx.Wait(t) 195 | 196 | resp, err := http.Get(nginx.URL() + "/") // ③ 197 | if err != nil { 198 | t.Fatal(err) 199 | } 200 | defer resp.Body.Close() 201 | 202 | if resp.StatusCode != http.StatusOK { 203 | t.Errorf("status code should be 200, but %d", resp.StatusCode) 204 | } 205 | 206 | body, err := ioutil.ReadAll(resp.Body) 207 | if err != nil { 208 | t.Fatal(err) 209 | } 210 | if string(body) != "I am AP server" { 211 | t.Errorf("unexpected response body: %s", string(body)) 212 | } 213 | } 214 | ``` 215 | 216 | ① `StartMockAP()` でモックAPを起動しています。 217 | 218 | ② `StartNginx()` で nginx コンテナを起動します。ここでモックAPのアドレスを差し込んでいることに注目してください。 219 | 220 | ③ AP と nginx を起動できたら、あとはもう普通のテストです。リクエストを送り、レスポンスを普通に assert しましょう。 221 | 222 | MockAP を Nginx に差し込んでいる部分は OOP における Dependency Injection に相当します。 223 | 普通、Dependecy Injection は同じプロセス内のオブジェクトに対して行うのですが、この例だと別のプロセスに対して依存オブジェクトを差し込んでいる形になっているのが面白いところです。 224 | 225 | それでは最後に `StartMockAP()` の実装を見てみましょう。 226 | 227 | ## StartMockAP() 228 | 229 | `StartMockAP()` は以下のようになっています(一部説明に不要な部分を省略しています)。 230 | ポートを自動的に選ぶためにちょっと特殊なことをしていることを除けば、単に goroutine で HTTP サーバーを立てているだけです。 231 | 232 | ```go 233 | // 空いているポートを自動的に選ぶ 234 | l, err := net.Listen("tcp", ":0") 235 | if err != nil { 236 | t.Fatal(err) 237 | } 238 | 239 | handler := func(w http.ResponseWriter, req *http.Request) { 240 | w.Write([]byte("I am AP server")) 241 | } 242 | ap := &MockAP{ 243 | host: host, 244 | port: l.Addr().(*net.TCPAddr).Port, 245 | server: &http.Server{ 246 | Handler: http.HandlerFunc(handler), 247 | }, 248 | } 249 | 250 | // 別の goroutine でサーバーを走らせる 251 | go func() { 252 | if err := ap.server.Serve(l); err != nil && err != http.ErrServerClosed { 253 | t.Log(err) 254 | } 255 | }() 256 | ``` 257 | 258 | なお、標準ライブラリの `httptest` で `StartMockAP` と同じようなことができますが、`httptest` はアドレスを `127.0.0.1` にバインドしてしまうので、今回のユースケースでは利用できません。 259 | 260 | 今回のサンプルコードでは一種類の MockAP しか実装されていませんが、私達が実際に使っているテストでは様々な MockAP が実装されています。 261 | 例えば、レスポンスを一切返さない MockAP や、不正な SSL 証明書を持つ MockAP などがあります。 262 | これらの MockAP を使うことで、手動で起こすのが面倒なケースをテストすることができます。 263 | 264 | ## まとめ 265 | 266 | Go と Docker を使って nginx をテストする仕組みを紹介しました。 267 | この仕組みは、我々のプロダクション環境における nginx を支えています。 268 | 269 | この記事がみなさまの nginx ライフの一助となれば幸いです。 270 | -------------------------------------------------------------------------------- /circleci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybozu/SAMPLE-test-nginx-with-go-and-docker/1add1870e5621631e33bff33f0089e27ffc46f7e/circleci.png -------------------------------------------------------------------------------- /fig.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybozu/SAMPLE-test-nginx-with-go-and-docker/1add1870e5621631e33bff33f0089e27ffc46f7e/fig.pptx -------------------------------------------------------------------------------- /sample-nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17.9 2 | 3 | COPY nginx.conf.tmpl /etc/nginx/nginx.conf.tmpl 4 | COPY entrypoint /entrypoint 5 | 6 | CMD ["/entrypoint"] 7 | -------------------------------------------------------------------------------- /sample-nginx/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | envsubst '$AP_SERVER_ADDR' \ 4 | < /etc/nginx/nginx.conf.tmpl \ 5 | > /etc/nginx/nginx.conf 6 | 7 | exec /usr/sbin/nginx -g "daemon off;" 8 | -------------------------------------------------------------------------------- /sample-nginx/nginx.conf.tmpl: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | events {} 4 | 5 | http { 6 | server { 7 | listen 80; 8 | 9 | location / { 10 | proxy_pass http://${AP_SERVER_ADDR}; 11 | proxy_set_header X-Request-Id $request_id; 12 | } 13 | 14 | location = /health { 15 | return 200 "OK"; 16 | } 17 | 18 | location = /robots.txt { 19 | return 200 "User-agent: *\nDisallow: /\n"; 20 | } 21 | 22 | location /secret/ { 23 | deny all; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nojima/test-nginx-with-go-amd-docker 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /test/mock_ap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "os" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | // MockAP は AP サーバーのモック 13 | type MockAP struct { 14 | server *http.Server 15 | 16 | host string 17 | port int 18 | 19 | lastRequest *http.Request 20 | mutex sync.Mutex 21 | } 22 | 23 | // StartMockAP は MockAP を起動する。 24 | func StartMockAP(t *testing.T) *MockAP { 25 | host := os.Getenv("TESTER_NAME") 26 | if host == "" { 27 | t.Fatal("Please specify TESTER_NAME") 28 | } 29 | 30 | l, err := net.Listen("tcp", ":0") // 空いているポートを自動的に選ぶ 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | ap := &MockAP{ 36 | host: host, 37 | port: l.Addr().(*net.TCPAddr).Port, 38 | } 39 | handler := func(w http.ResponseWriter, req *http.Request) { 40 | ap.mutex.Lock() 41 | defer ap.mutex.Unlock() 42 | ap.lastRequest = req 43 | 44 | w.Write([]byte("I am AP server")) 45 | } 46 | ap.server = &http.Server{ 47 | Handler: http.HandlerFunc(handler), 48 | } 49 | 50 | // 別の goroutine でサーバーを走らせる 51 | go func() { 52 | if err := ap.server.Serve(l); err != nil && err != http.ErrServerClosed { 53 | t.Log(err) 54 | } 55 | }() 56 | 57 | return ap 58 | } 59 | 60 | // Address は MockAP にアクセスするためのアドレスを返す。 61 | func (a *MockAP) Address() string { 62 | return fmt.Sprintf("%s:%d", a.host, a.port) 63 | } 64 | 65 | // LastRequest は最後に受け取ったリクエストを返す。 66 | // リクエストをまだ受け取っていない場合は nil を返す。 67 | func (a *MockAP) LastRequest() *http.Request { 68 | a.mutex.Lock() 69 | defer a.mutex.Unlock() 70 | return a.lastRequest 71 | } 72 | 73 | // Close は MockAP を破棄する。 74 | func (a *MockAP) Close(t *testing.T) { 75 | if err := a.server.Close(); err != nil { 76 | t.Log(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/nginx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | // Nginx はテスト対象となる nginx のプロセスを表す。 16 | type Nginx struct { 17 | cmd *exec.Cmd 18 | containerName string 19 | exited chan int // 終了したら ExitCode が入る 20 | } 21 | 22 | // NginxConfig は nginx を起動するために必要なオプションを保持する。 23 | type NginxConfig struct { 24 | APServerAddress string 25 | } 26 | 27 | // StartNginx は新しい nginx のプロセスを起動する。 28 | func StartNginx(t *testing.T, config NginxConfig) *Nginx { 29 | name := "nginx-" + randomSuffix() 30 | 31 | network := os.Getenv("DOCKER_NETWORK") 32 | if network == "" { 33 | t.Fatal("Please specify DOCKER_NETWORK") 34 | } 35 | 36 | if config.APServerAddress == "" { 37 | // 誰も listen していないポートを指定することで bad gateway になるようにする 38 | config.APServerAddress = "localhost:9999" 39 | } 40 | 41 | // docker コマンドを叩いて sample-nginx:latest を起動する。 42 | args := []string{ 43 | "run", "--rm", 44 | "--name", name, 45 | "--net", network, 46 | "-e", fmt.Sprintf("AP_SERVER_ADDR=%s", config.APServerAddress), 47 | "sample-nginx:latest", 48 | } 49 | cmd := exec.Command("docker", args...) 50 | cmd.Stdout = os.Stdout 51 | cmd.Stderr = os.Stdout 52 | if err := cmd.Start(); err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | nginx := &Nginx{ 57 | cmd: cmd, 58 | containerName: name, 59 | exited: make(chan int), 60 | } 61 | 62 | // nginx プロセスが終了したら nginx.exited に終了ステータスを入れるようにする 63 | go func() { 64 | if err := cmd.Wait(); err != nil { 65 | t.Log(err) 66 | } 67 | nginx.exited <- cmd.ProcessState.ExitCode() 68 | close(nginx.exited) 69 | }() 70 | 71 | return nginx 72 | } 73 | 74 | // URL は nginx にアクセスするための URL を返す。 75 | func (n *Nginx) URL() string { 76 | return fmt.Sprintf("http://%s:80", n.containerName) 77 | } 78 | 79 | // Wait は nginx が起動するまで待つ。 80 | func (n *Nginx) Wait(t *testing.T) { 81 | maxRetry := 20 82 | for i := 0; i < maxRetry; i++ { 83 | t.Logf("Wait for nginx... (%d/%d)", i, maxRetry) 84 | 85 | // nginx が死んでないか調べる。 86 | // この時点で死んでいたらテストを fail させる。 87 | select { 88 | case exitCode := <-n.exited: 89 | t.Fatalf("nginx exited unexpectedly: exitCode=%d", exitCode) 90 | default: 91 | } 92 | 93 | // ヘルスチェック用エンドポイントを叩いて何か返ってきたら起動したものとする。 94 | resp, err := http.Get(n.URL() + "/health") 95 | if err != nil { 96 | time.Sleep(500 * time.Millisecond) 97 | continue 98 | } 99 | resp.Body.Close() 100 | return 101 | } 102 | } 103 | 104 | // Close は nginx を終了する。 105 | func (n *Nginx) Close(t *testing.T) { 106 | cmd := exec.Command("docker", "kill", n.containerName) 107 | cmd.Stdout = os.Stdout 108 | cmd.Stderr = os.Stdout 109 | if err := cmd.Run(); err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | <-n.exited // 終了するまで待つ 114 | } 115 | 116 | // randomSuffix はコンテナ名の suffix として使うためのランダム文字列を返す。 117 | // suffix をつけるのは、テストを並列実行したときにコンテナの名前が被らないようにするため。 118 | func randomSuffix() string { 119 | b := make([]byte, 6) 120 | _, err := rand.Read(b) 121 | if err != nil { 122 | panic(err) 123 | } 124 | enc := base32.StdEncoding.WithPadding(base32.NoPadding) 125 | return strings.ToLower(enc.EncodeToString(b)) 126 | } 127 | -------------------------------------------------------------------------------- /test/nginx_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | // リバプロせずに nginx が直接レスポンスを返すパターンのテスト 10 | func TestStaticResponses(t *testing.T) { 11 | t.Parallel() 12 | 13 | nginx := StartNginx(t, NginxConfig{}) 14 | defer nginx.Close(t) 15 | nginx.Wait(t) 16 | 17 | t.Run("robots.txt should be available", func(t *testing.T) { 18 | resp, err := http.Get(nginx.URL() + "/robots.txt") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer resp.Body.Close() 23 | 24 | if resp.StatusCode != http.StatusOK { 25 | t.Errorf("status code should be 200, but %d", resp.StatusCode) 26 | } 27 | 28 | body, err := ioutil.ReadAll(resp.Body) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | expected := "User-agent: *\nDisallow: /\n" 33 | if string(body) != expected { 34 | t.Errorf("unexpected response body: %s", string(body)) 35 | } 36 | }) 37 | 38 | t.Run("access to /secret/ should be denied", func(t *testing.T) { 39 | resp, err := http.Get(nginx.URL() + "/secret/") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer resp.Body.Close() 44 | 45 | if resp.StatusCode != http.StatusForbidden { 46 | t.Errorf("status code should be 400, but %d", resp.StatusCode) 47 | } 48 | }) 49 | } 50 | 51 | // APサーバーにリバプロするパターンのテスト 52 | func TestReverseProxy(t *testing.T) { 53 | t.Parallel() 54 | 55 | ap := StartMockAP(t) 56 | defer ap.Close(t) 57 | 58 | nginx := StartNginx(t, NginxConfig{ 59 | APServerAddress: ap.Address(), 60 | }) 61 | defer nginx.Close(t) 62 | nginx.Wait(t) 63 | 64 | t.Run("response should be returned from AP", func(t *testing.T) { 65 | resp, err := http.Get(nginx.URL() + "/") 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | defer resp.Body.Close() 70 | 71 | if resp.StatusCode != http.StatusOK { 72 | t.Errorf("status code should be 200, but %d", resp.StatusCode) 73 | } 74 | 75 | body, err := ioutil.ReadAll(resp.Body) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if string(body) != "I am AP server" { 80 | t.Errorf("unexpected response body: %s", string(body)) 81 | } 82 | }) 83 | 84 | t.Run("X-Request-Id should be sent to AP", func(t *testing.T) { 85 | resp, err := http.Get(nginx.URL() + "/") 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | defer resp.Body.Close() 90 | 91 | requestID := ap.LastRequest().Header.Get("X-Request-Id") 92 | if requestID == "" { 93 | t.Error("X-Request-Id header does not exist") 94 | } 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | cd "$(dirname "$0")" 4 | 5 | docker network create nginx-test || true 6 | 7 | # CircleCI 上では setup_remote_docker で立ち上げたコンテナに primary container から通信する手段が存在しないので、 8 | # テスト全体を remote docker 上で実行する 9 | docker create -v /project --name nginx-test-data busybox:latest /bin/true 10 | trap "docker rm nginx-test-data" EXIT 11 | docker cp . nginx-test-data:/project 12 | 13 | echo "-------------------------------------------------------------------------" 14 | 15 | docker run \ 16 | --volumes-from nginx-test-data \ 17 | --net nginx-test \ 18 | --name nginx-tester \ 19 | --rm \ 20 | -v /var/run/docker.sock:/var/run/docker.sock \ 21 | -w /project \ 22 | -e "DOCKER_NETWORK=nginx-test" \ 23 | -e "TESTER_NAME=nginx-tester" \ 24 | --user root \ 25 | circleci/golang:1.14 \ 26 | go test -v ./... 27 | --------------------------------------------------------------------------------