概要
本記事では、Go 公式にある Web フレームワーク Gin を利用して作成する RESTful API に testing, httptest を使用したテストを作成します。
環境
- Ubuntu 24.04.1 LTS (Oracle VM Virtualbox 上に構築しております)
- Go 1.23.3
ライブラリバージョン
- Gin 1.10
本題
Go のインストール
公式の下記ページを参考にインストールを実施します。
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 を作成します。
チュートリアルを実施すると、以下3つの API とコードが完成します。
- (GET) /albums ← サーバーが持つアルバムの情報を JSON で取得する API
- (GET) /albums/:id ← URL で指定した ID のアルバム情報を JSON で取得する API
- (POST) /albums ← サーバーが持つアルバムの情報に、リクエストで指定した情報を追加する API
チュートリアルを行うことで完成するコード
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 レスポンスステータスコードが想定通りかを確認したいと思います。
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 レスポンスステータスコードが想定通りかを確認したいと思います。
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 レスポンスステータスコードが想定通りかを確認したいと思います。
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))
}
}
完成形
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つのテストを実行した結果が表示されます。
また、 go test -v .
と実行すると、詳細なテストの結果が表示されます。
結果
今回用意したテストコードで、 3 つの API が動作することが確認できました。
ですが、今回用意したテストケースでは、 API が正しく動作するのかの確認ができません。
例えば、 (GET) /albums/:id API で、 2 以外の ID を送ったときに、対応したアルバム情報を返すのか?
(POST) /albums API に、2 回別々の情報を送った際に、2 つ分のアルバム情報が追加されているか?
この通り、いくつか懸念事項が残っております。
終わりに
次回の記事では、今回用意した RESTful API を拡張しつつ、テストコードも充実させていこうと思います。
参考資料
Go 公式ページ
Go RESTful API チュートリアルページ
Gin Web Framework