RESTful のチュートリアルのテストコードの作成

## 概要

本記事では、Go 公式にある Web フレームワーク Gin を利用して作成する RESTful API に testing, httptest を使用したテストを作成します。

## 環境

* Ubuntu 24.04.1 LTS （Oracle VM Virtualbox 上に構築しております）
* Go 1.23.3

ライブラリバージョン
* Gin 1.10

## 本題

### Go のインストール

公式の下記ページを参考にインストールを実施します。

https://go.dev/doc/install

Ubuntu を利用しているので、上記 URL の Linux の手順に従ってインストールします。

※ リンク先の Linux タブにある 「Note:」 の通り `$HOME/.profile` or `/etc/profile` に `export PATH=$PATH:/usr/local/go/bin` を追記しただけでは、 `go version` が実行できない可能性があります。  
その場合は、 `source $HOME/.profile` or `source /etc/profile` を実行して追記した設定を反映させてください。

### RESTful API の作成

公式の下記ページにある、 Gin Web Framework を用いた、 RESTful API を作成します。

https://go.dev/doc/tutorial/web-service-gin

チュートリアルを実施すると、以下3つの API とコードが完成します。

* (GET) /albums ← サーバーが持つアルバムの情報を JSON で取得する API
* (GET) /albums/:id ← URL で指定した ID のアルバム情報を JSON で取得する API
* (POST) /albums ← サーバーが持つアルバムの情報に、リクエストで指定した情報を追加する API

チュートリアルを行うことで完成するコード

```go main.go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

var albums = []album{
	{
		ID:     "1",
		Title:  "Blue Train",
		Artist: "John Coltrane",
		Price:  56.99,
	},
	{
		ID:     "2",
		Title:  "Jeru",
		Artist: "Gerry Mulligan",
		Price:  17.99,
	},
	{
		ID:     "3",
		Title:  "Sarah Vaughan and Clifford Brown",
		Artist: "Sarah Vaughan",
		Price:  39.99,
	},
}

func getAlbums(c *gin.Context) {
	c.JSON(http.StatusOK, albums)
}

func getAlbumByID(c *gin.Context) {
	id := c.Param("id")

	for _, a := range albums {
		if a.ID == id {
			c.JSON(http.StatusOK, a)
			return
		}
	}
	c.JSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

func postAlbums(c *gin.Context) {
	var newAlbum album
	if err := c.BindJSON(&newAlbum); err != nil {
		return
	}

	albums = append(albums, newAlbum)
	c.JSON(http.StatusOK, albums)
}

func setupRouter() *gin.Engine {
	r := gin.Default()
	return r
}

func main() {
	router := setupRouter()
	router.GET("/albums", getAlbums)
	router.GET("/albums/:id", getAlbumByID)
	router.POST("/albums", postAlbums)

	router.Run(":8080")
}

```

### テストコードの作成

#### テストコードの検討

今回用意した 3 つの API で作成するテストについて検討しましょう。

*  (GET) /albums API
    * 全てのアルバム情報を JSON で返す API です。テストコードでは、API が返すアルバムの情報が、想定しているものと一致しているかを確認します
*  (GET) /albums/:id API
    * URL で指定した ID を持つアルバム情報を返す API です。テストコードでは、 API が返すアルバムの情報が、指定した ID のものと一致しているかを確認します
*  (POST) /albums API
    * サーバーが持つアルバム情報に、 POST で送った情報を追加する API です。テストコードでは、 POST で送った情報が追加されているかを確認します。


#### (GET) /albums API

(GET) /albums API は、全てのアルバム情報を返す API です。  
なので、テストコードでは、 API レスポンスの内容と、 HTTP レスポンスステータスコードが想定通りかを確認したいと思います。

```go main_test.go

package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// テストコードは Test~~ という名前にします。この名称を付けることで、テストとして実行されるようになります。
func TestGetAlbums(t *testing.T) {

    // テスト用の HTTP リクエストを記録する構造体の設定
	w := httptest.NewRecorder()
	req, err := http.NewRequest("GET", "/albums", nil)
	if err != nil {
		t.Fatal(err)
	}

    // テスト用のサーバー設定。 /albums で、全アルバムの情報を返すよう設定
	router := setupRouter()
	router.GET("/albums", getAlbums)

    // サーバーの起動
	router.ServeHTTP(w, req)

    // API が返すレスポンスの内容を設定
	expected, err := json.Marshal(albums)
	if err != nil {
		t.Fatal(err)
	}

    // HTTP のレスポンスコードが想定通りか判定
	if http.StatusOK != w.Code {
		t.Errorf("HTTP response code is not match: result (%d), want (%d)", w.Code, http.StatusOK)
	}

    // HTTP のレスポンスの内容が想定通りか判定。 JSON のままだと比較ができないため、文字列に変換して内容が一致しているか判定しています。
	if w.Body.String() != string(expected) {
		t.Errorf("HTTP response is not match: result(%s), want (%s)", w.Body.String(), string(expected))
	}

}
```

#### (GET) /albums/:id API

(GET) albums API は、リクエストで指定した ID のアルバム情報を返す API です。  
テストコードでは、 指定した ID で返す API レスポンスの内容と、 HTTP レスポンスステータスコードが想定通りかを確認したいと思います。

```go main_test.go
func TestGetAlbumsByID(t *testing.T) {
	router := setupRouter()
	router.GET("/albums/:id", getAlbumByID)

	w := httptest.NewRecorder()
	req, err := http.NewRequest("GET", "/albums/2", nil)
	if err != nil {
		t.Fatal(err)
	}
	router.ServeHTTP(w, req)

    // ID 2 が API が返すレスポンスの内容を設定
	expected, err := json.Marshal(album{
		ID:     "2",
		Title:  "Jeru",
		Artist: "Gerry Mulligan",
		Price:  17.99,
	})
	if err != nil {
		t.Fatal(err)
	}

	if http.StatusOK != w.Code {
		t.Errorf("HTTP response code is not match: result (%d), want (%d)", w.Code, http.StatusOK)
	}

	if w.Body.String() != string(expected) {
		t.Errorf("HTTP response is not match: result(%s), want (%s)", w.Body.String(), string(expected))
	}
}
```

#### (POST) /albums API

(POST) /albums は、新しいアルバムの情報を追加する API です。アルバム情報を追加した後で、サーバーが持つ全ての情報をレスポンスとして返します。
テストコードでは、レスポンスが、あらかじめ設定されているアルバムの情報と、新しいアルバム情報両方が設定されているかと、HTTP レスポンスステータスコードが想定通りかを確認したいと思います。

```go main_test.go
func TestPostAlbums(t *testing.T) {

	beforeAlbums := albums

    // サーバに送るデータの準備
	postData := album{
		ID:     "4",
		Title:  "Test Title",
		Artist: "Test Taro",
		Price:  100,
	}
	postJson, err := json.Marshal(postData)
	if err != nil {
		t.Fatal(err)
	}

	req, err := http.NewRequest("POST", "/albums/", strings.NewReader(string(postJson)))
	if err != nil {
		t.Fatal(err)
	}
	router := setupRouter()
	router.POST("/albums/", postAlbums)

	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if http.StatusOK != w.Code {
		t.Errorf("HTTP response code is not match: result (%d), want (%d)", w.Code, http.StatusOK)
	}

    // API が返すレスポンスの内容を設定。 サーバーであらかじめ設定しているアルバムの情報に、今回 Post で送るデータを追加する。
	expected, err := json.Marshal(append(beforeAlbums, postData))
	if err != nil {
		t.Fatal(err)
	}

	if w.Body.String() != string(expected) {
		t.Errorf("HTTP response is not match: result(%s), want (%s)", w.Body.String(), string(expected))
	}
}

```

#### 完成形

```go main_test.go
package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestGetAlbums(t *testing.T) {

	w := httptest.NewRecorder()
	req, err := http.NewRequest("GET", "/albums", nil)
	if err != nil {
		t.Fatal(err)
	}

	router := setupRouter()
	router.GET("/albums", getAlbums)
	router.ServeHTTP(w, req)

	expected, err := json.Marshal(albums)
	if err != nil {
		t.Fatal(err)
	}

	if http.StatusOK != w.Code {
		t.Errorf("HTTP response code is not match: result (%d), want (%d)", w.Code, http.StatusOK)
	}

	if w.Body.String() != string(expected) {
		t.Errorf("HTTP response is not match: result(%s), want (%s)", w.Body.String(), string(expected))
	}

}

func TestGetAlbumsByID(t *testing.T) {
	router := setupRouter()
	router.GET("/albums/:id", getAlbumByID)

	w := httptest.NewRecorder()
	req, err := http.NewRequest("GET", "/albums/2", nil)
	if err != nil {
		t.Fatal(err)
	}
	router.ServeHTTP(w, req)

	expected, err := json.Marshal(album{
		ID:     "2",
		Title:  "Jeru",
		Artist: "Gerry Mulligan",
		Price:  17.99,
	})
	if err != nil {
		t.Fatal(err)
	}

	if http.StatusOK != w.Code {
		t.Errorf("HTTP response code is not match: result (%d), want (%d)", w.Code, http.StatusOK)
	}

	if w.Body.String() != string(expected) {
		t.Errorf("HTTP response is not match: result(%s), want (%s)", w.Body.String(), string(expected))
	}
}

func TestPostAlbums(t *testing.T) {

	beforeAlbums := albums

	postData := album{
		ID:     "4",
		Title:  "Test Title",
		Artist: "Test Taro",
		Price:  100,
	}
	postJson, err := json.Marshal(postData)
	if err != nil {
		t.Fatal(err)
	}

	req, err := http.NewRequest("POST", "/albums/", strings.NewReader(string(postJson)))
	if err != nil {
		t.Fatal(err)
	}
	router := setupRouter()
	router.POST("/albums/", postAlbums)

	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if http.StatusOK != w.Code {
		t.Errorf("HTTP response code is not match: result (%d), want (%d)", w.Code, http.StatusOK)
	}

	expected, err := json.Marshal(append(beforeAlbums, postData))
	if err != nil {
		t.Fatal(err)
	}

	if w.Body.String() != string(expected) {
		t.Errorf("HTTP response is not match: result(%s), want (%s)", w.Body.String(), string(expected))
	}
}

```

main_test.go があるディレクトリに移動して、 `go test .` と実行すると、今回用意した3つのテストを実行した結果が表示されます。

実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934384/04e12748-5874-581b-e465-14d784b53265.png)

また、 `go test -v . ` と実行すると、詳細なテストの結果が表示されます。

#### 結果

今回用意したテストコードで、 3 つの API が動作することが確認できました。
ですが、今回用意したテストケースでは、 API が正しく動作するのかの確認ができません。  

例えば、 (GET) /albums/:id API で、 2 以外の ID を送ったときに、対応したアルバム情報を返すのか？  
 (POST) /albums API に、2 回別々の情報を送った際に、2 つ分のアルバム情報が追加されているか？
この通り、いくつか懸念事項が残っております。

## 終わりに

次回の記事では、今回用意した RESTful API を拡張しつつ、テストコードも充実させていこうと思います。

## 参考資料

Go 公式ページ

https://go.dev/doc/install

Go RESTful API チュートリアルページ

https://go.dev/doc/tutorial/web-service-gin

Gin Web Framework 

https://gin-gonic.com/ja/