Prismで建てたmockサーバーを使ってGoのリクエストをテストする

# はじめに
外部サービスへのHTTPリクエスト処理をテストする場合、mockサーバーを使うことが多いと思います。
挙動確認目的で実際にサーバーに何度もリクエストを送ると迷惑をかけてしまいますからね。

そこでこの記事ではYahooのTokenエンドポイントを例に、Prismでmockサーバーを建ててGoのリクエストをテストしてみたいと思います。

# 実装
dockerでprismイメージからmockサーバーを構築して、そのmockサーバーにリクエストを送ります。
リクエスト時のパラメータや想定されるレスポンスは、[Yahoo!デベロッパーネットワーク（Tokenエンドポイント）](https://developer.yahoo.co.jp/yconnect/v2/authorization_code/token.html)を参考にしています。
※以下のコードで実際にYahoo Tokenエンドポイントに正しくリクエストできるかは確認していないのであしからず。あくまでテストに焦点を当てた内容となっています。

まずはメインロジックとなるgoファイルから見ていきます。

```go:main.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
)

func main() {
	var tokenParams TokenParams
	endpoint := "https://auth.login.yahoo.co.jp/yconnect/v2/token" //YahooのTokenエンドポイント
	res, e := requestTokenEndpoint(endpoint)
	if e != nil {
		log.Fatalln(e)
	}

	body, e := io.ReadAll(res.Body)
	if e != nil {
		log.Fatalln(e)
	}
	e = json.Unmarshal([]byte(body), &tokenParams)
	if e != nil {
		log.Fatalln(e)
	}
	fmt.Printf("%+v\n", tokenParams)
}

```

```go:request_token_endpoint.go
package main

import (
	"net/http"
	"net/url"
	"strings"
)

// レスポンスパラメータをマッピングするためのstruct
type TokenParams struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int    `json:"expires_in"`
	IdToken      string `json:"id_token"`
}

func requestTokenEndpoint(endpoint string) (res *http.Response, e error) {
    // リクエストボディに含めるパラメータ
	values := url.Values{}
	values.Add("grant_type", "authorization_code")
	values.Add("redirect_uri", "https://example.com")
	values.Add("code", "SxlOBeZQ")

	req, e := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(values.Encode()))
	if e != nil {
		return nil, e
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Authorization", "Basic SlAV32hkKG")

	client := &http.Client{}
	res, e = client.Do(req)
	if e != nil {
		return nil, e
	}

	return res, nil
}
```

```go:request_token_endpoint_test.go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"testing"
)

func TestRequestTokenEndpoint(t *testing.T) {
	var tokenParams TokenParams
	url := "http://localhost:4010/yahoo-token" // ローカルのmockサーバーのエンドポイント
	expectedAccessToken := "SlAV32hkKG" // mockサーバーから返ってくるaccess_token

	t.Run("success test", func(t *testing.T) {
		res, e := requestTokenEndpoint(url)
		if e != nil {
			t.Error(e)
		}
		defer res.Body.Close()

    	if res.StatusCode != http.StatusOK {
			t.Errorf("mock server return status code: %s", res.Status)
		}

		body, _ := io.ReadAll(res.Body)
		e = json.Unmarshal(body, &tokenParams)
		if e != nil {
			t.Error(e)
		}
		if expectedAccessToken != "SlAV32hkKG" {
			t.Errorf("expected access_token is %s, but got %s", expectedAccessToken, tokenParams.AccessToken)
		}

		fmt.Printf("%+v\n", tokenParams)
	})
}
```


次に構築するmockサーバーの定義を見ていきます。

```yaml:openapi.yml
openapi: "3.0.0"
info:
  version: 1.0.0
  title: prism sample
  description: prism sample
servers:
  - url: http://localhost:4010
    description: local mock server
paths:
  /yahoo-token:
    post:
      description: return token parameters
      requestBody:
        content:
          "application/x-www-form-urlencoded":
            schema:
              type: object
              required:
                - grant_type
                - redirect_uri
                - code
              properties:
                grant_type:
                  type: string
                redirect_uri:
                  type: string
                code:
                  type: string
      responses:
        "200":
          description: success
          content:
            "application/json":
              schema:
                type: object
                properties:
                  access_token:
                    type: string
                    example: SlAV32hkKG
                  token_type:
                    type: string
                    example: Bearer
                  refresh_token:
                    type: string
                    example: 8xLOxBtZp8
                  expires_in:
                    type: integer
                    example: 3600 
                  id_token:
                    type: string
                    example: eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg
```
`paths.post.requestBody`にリクエスト時に必要なパラメータを定義します。すべてのパラメータをrequiredに記述しているので、リクエスト時にパラメータが一つでも欠けていると422ステータスが返ります。
そして、`paths.post.responses`にレスポンスとして返すパラメータを定義しています。

最後にdocker-comopseを見ていきます。
```yaml:docker-compose.yml
version: "3"
services:
  swagger-server:
    image: stoplight/prism:3
    container_name: "swagger-server"
    ports:
      - "4010:4010"
    command: mock -h 0.0.0.0 /openapi.yml
    volumes:
      - ./openapi.yml:/openapi.yml
```
prismイメージをpullして4010ポートで受け付けます。また、先ほど定義した`openapi.yml`をマウントしてmockコマンドを実行することで、定義に沿ったmockサーバーをローカルに構築します。


これでテストの準備ができました！
`docker-compose up`してmockサーバーを立ち上げてみましょう。
```
$ docker-compose up

swagger-server    | [2:54:03 PM] › [CLI] …  awaiting  Starting Prism…
swagger-server    | [2:54:07 PM] › [CLI] ℹ  info      POST       http://0.0.0.0:4010/yahoo-token
swagger-server    | [2:54:07 PM] › [CLI] ▶  start     Prism is listening on http://0.0.0.0:4010
```

mockサーバーが立ち上がったので、`go test`でリクエストを送ってみます。

```
$ go test -v

=== RUN   TestRequestTokenEndpoint
=== RUN   TestRequestTokenEndpoint/success_test
{AccessToken:SlAV32hkKG TokenType:Bearer RefreshToken:8xLOxBtZp8 ExpiresIn:3600 IdToken:eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg}
--- PASS: TestRequestTokenEndpoint (0.16s)
    --- PASS: TestRequestTokenEndpoint/success_test (0.16s)
PASS
ok      github.com/pae26/go-prism       0.482s
```

無事テストが通って、`openapi.yml`で定義したレスポンスパラメータ（`access_token`や`token_type`など）が返ってきてることが確認できました！

試しにリクエストパラメータを減らしてみます。
```go:request_token_endpoint.go
...
func requestTokenEndpoint(endpoint string) (res *http.Response, e error) {
	values := url.Values{}
	// values.Add("grant_type", "authorization_code")
	values.Add("redirect_uri", "https://example.com")
	values.Add("code", "SxlOBeZQ")
...
```
```
$ go test -v

=== RUN   TestRequestTokenEndpoint
=== RUN   TestRequestTokenEndpoint/success_test
    request_token_endpoint_test.go:24: mock server return status code: 422 Unprocessable Entity
    request_token_endpoint_test.go:33: expected access_token is SlAV32hkKG, but got 
{AccessToken: TokenType: RefreshToken: ExpiresIn:0 IdToken:}
--- FAIL: TestRequestTokenEndpoint (0.03s)
    --- FAIL: TestRequestTokenEndpoint/success_test (0.03s)
FAIL
exit status 1
FAIL    github.com/pae26/go-prism       0.353s
```
`openapi.yml`のrequiredに定義したパラメータが足りていないので、`422 Unprocessable Entity`が返っていることがわかります。

# おわりに
mockサーバーを定義・構築し、Goのリクエストをテストする流れを見てきました。
Goをクライアントとして外部サーバーにリクエストする処理（今回扱ったソーシャルログインなど）を実装する際の参考になればいいなと思います。