こんにちは。KOUKIです。
とあるWeb系企業でエンジニアをやってます。
前回、Go言語のWebフレームワークであるFiberとORMラッパーであるGORMを使って簡単なCRUDアプリを実装しました。
その過程で、以下のエンドポイントを作成しました。
1 2 3 4 5 6 |
func setupRoutes(app *fiber.App) { app.Get("/api/v1/books", book.GetBooks) app.Get("/api/v1/book/:id", book.GetBook) app.Post("/api/v1/book", book.NewBook) app.Delete("/api/v1/book/:id", book.DeleteBook) } |
本記事では、上記のエンドポイントに向けてUnitTestする方法を考えてみました。
UnitTestは、プログラミング開発する上で避けては通れないものなので、必見です!

事前準備
main.goと同じディレクトリ上に、下記のファイルを作成しましょう。
また、assertパッケージも追加します。
1 2 3 4 5 |
// テストファイル touch main_test.go // テスト用のモジュール go get github.com/stretchr/testify/assert |
Go言語では、xxxx_test.goファイルにテストコードを実装します。
UnitTest ~HTTP リクエスト~
HTTP リクエストのUnitTestを実装していきましょう。
事前準備
1 2 3 4 5 6 7 |
package main import "testing" func Test_BOOK_HTTP_REQUEST(t *testing.T) { // ... Todo Test } |
Go言語では、Test_XXXXから始まる命名規則でテストコードを実装しなければなりません。
また、その引数には「testing」パッケージを指定します。
テストを実行するには、「go test」コマンドを使います。
※「-v」オプションにてテストの詳細を表示
1 2 3 4 5 |
$ go test -v === RUN Test_BOOK_HTTP_REQUEST --- PASS: Test_BOOK_HTTP_REQUEST (0.00s) PASS ok fiber-app 0.602s |
続いて、テストに必要なFiber App、データベース、ルーター、テストデータの準備をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package main import ( "bytes" "encoding/json" "fiber-app/book" "fiber-app/repository" "testing" "github.com/gofiber/fiber/v2" ) func Test_BOOK_HTTP_REQUEST(t *testing.T) { // 事前準備 app := fiber.New() // データベースを初期化 initDatabase() defer repository.DBConn.Close() // ルートをセット setupRoutes(app) // テストデータを用意 expectBody := book.Book{ Title: "UnitTest Post", Author: "selfnote", Rating: 3, } // jsonに変換 jsonBody, _ := json.Marshal(&expectBody) // bytesに変換 readerBody := bytes.NewBuffer(jsonBody) } |
POST Test
以下の順でテストをしたいと思います。
- POST リクエスト — データが保存されるか
- GET リクエスト — データが取得できるか
- DELETE リクエスト — データが削除されるか
そんなわけで、最初はPOSTリクエストのテストです。
Test_BOOK_HTTP_REQUEST関数に、下記のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package main import ( "bytes" "encoding/json" "fiber-app/book" "fiber-app/repository" "net/http" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) func Test_BOOK_HTTP_REQUEST(t *testing.T) { // 事前準備 ... // POST req, _ := http.NewRequest(http.MethodPost, "/api/v1/book", readerBody) req.Header.Set("Content-Type", "application/json") // POST テスト実行 resp, err := app.Test(req) var actual book.Book json.NewDecoder(resp.Body).Decode(&actual) // 結果確認 assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, expectBody.Title, actual.Title) assert.Equal(t, expectBody.Author, actual.Author) assert.Equal(t, expectBody.Rating, actual.Rating) } |
httpパッケージのNewRequestメソッドで、HTTPリクエストを作成してます。
この時忘れがちなのが、Headerの「“Content-Type”, “application/json”」です。
JSON形式でリクエストを送信しないとFiberのBodyParser処理で「Unprocessable Entity」エラーが発生します。
1 2 3 |
if err := c.BodyParser(book); err != nil { return c.Status(503).SendString(err.Error()) } |
そして、FiberのAppに組み込まれているTestメソッドで、HTTPリクエストを送信します。
テストを実行しましょう。
1 2 3 4 5 6 7 |
$ go test -v === RUN Test_BOOK_HTTP_REQUEST 2022/03/06 07:21:55 Database connection successfully opend! 2022/03/06 07:21:55 Database Migrated --- PASS: Test_BOOK_HTTP_REQUEST (0.00s) PASS ok fiber-app 0.581s |
Greatですね、こいつは!
Get Test
同じ要領で、Getリクエストもテストします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func Test_BOOK_HTTP_REQUEST(t *testing.T) { ... // Get - GetBook url := fmt.Sprintf("/api/v1/book/%d", actual.ID) req, _ = http.NewRequest(http.MethodGet, url, nil) // Get テスト実行 resp, err = app.Test(req) var actual2 book.Book json.NewDecoder(resp.Body).Decode(&actual2) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, expectBody.Title, actual2.Title) assert.Equal(t, expectBody.Author, actual2.Author) assert.Equal(t, expectBody.Rating, actual2.Rating) // Get - GetBooks req, _ = http.NewRequest(http.MethodGet, "/api/v1/books", nil) resp, err = app.Test(req) var actual3 []book.Book json.NewDecoder(resp.Body).Decode(&actual3) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, expectBody.Title, actual3[0].Title) assert.Equal(t, expectBody.Author, actual3[0].Author) assert.Equal(t, expectBody.Rating, actual3[0].Rating) } |
Getは、URLに渡されたIDをKeyに一意のBookを取得するものと全てのBookを取得するものとの2つのエンドポイントがあるので、上記のように実装しました。
テストを実行します。
1 2 3 4 5 6 7 |
$ go test -v === RUN Test_BOOK_HTTP_REQUEST 2022/03/06 07:26:59 Database connection successfully opend! 2022/03/06 07:26:59 Database Migrated --- PASS: Test_BOOK_HTTP_REQUEST (0.01s) PASS ok fiber-app 0.578s |
OKですね。
DELETE Test
最後に、DELETEのテストです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func Test_BOOK_HTTP_REQUEST(t *testing.T) { ... // DELETE req, _ = http.NewRequest(http.MethodDelete, url, nil) resp, err = app.Test(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) // Getリクエストを送信してデータがなくなっているかチェックする req, _ = http.NewRequest(http.MethodGet, url, nil) resp, err = app.Test(req) var actual4 book.Book json.NewDecoder(resp.Body).Decode(&actual4.ID) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, "", actual4.Title) assert.Equal(t, "", actual4.Author) assert.Equal(t, 0, actual4.Rating) } |
DELETEの場合は、一度データを削除してからGETリクエストを送ってデータが消えているか確認してみました。
1 2 3 4 5 6 7 8 |
$ go test -v === RUN Test_BOOK_HTTP_REQUEST 2022/03/06 07:55:18 Database connection successfully opend! 2022/03/06 07:55:18 Database Migrated 2022/03/06 07:55:18 [{{4 2022-03-06 07:55:18.730383 +0900 JST 2022-03-06 07:55:18.730383 +0900 JST <nil>} UnitTest Post selfnote 3}] --- PASS: Test_BOOK_HTTP_REQUEST (0.01s) PASS ok fiber-app 0.194s |
OKですね。
リファクタリング
最後にリファクタリングをしてみましょうか。
現状は、以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
package main import ( "bytes" "encoding/json" "fiber-app/book" "fiber-app/repository" "fmt" "net/http" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) func Test_BOOK_HTTP_REQUEST(t *testing.T) { app := fiber.New() initDatabase() defer repository.DBConn.Close() setupRoutes(app) expectBody := book.Book{ Title: "UnitTest Post", Author: "selfnote", Rating: 3, } jsonBody, _ := json.Marshal(&expectBody) readerBody := bytes.NewBuffer(jsonBody) // POST req, _ := http.NewRequest(http.MethodPost, "/api/v1/book", readerBody) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) var actual book.Book json.NewDecoder(resp.Body).Decode(&actual) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, expectBody.Title, actual.Title) assert.Equal(t, expectBody.Author, actual.Author) assert.Equal(t, expectBody.Rating, actual.Rating) // Get - GetBook url := fmt.Sprintf("/api/v1/book/%d", actual.ID) req, _ = http.NewRequest(http.MethodGet, url, nil) resp, err = app.Test(req) var actual2 book.Book json.NewDecoder(resp.Body).Decode(&actual2) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, expectBody.Title, actual2.Title) assert.Equal(t, expectBody.Author, actual2.Author) assert.Equal(t, expectBody.Rating, actual2.Rating) // Get - GetBooks req, _ = http.NewRequest(http.MethodGet, "/api/v1/books", nil) resp, err = app.Test(req) var actual3 []book.Book json.NewDecoder(resp.Body).Decode(&actual3) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, expectBody.Title, actual3[0].Title) assert.Equal(t, expectBody.Author, actual3[0].Author) assert.Equal(t, expectBody.Rating, actual3[0].Rating) // DELETE req, _ = http.NewRequest(http.MethodDelete, url, nil) resp, err = app.Test(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) req, _ = http.NewRequest(http.MethodGet, url, nil) resp, err = app.Test(req) var actual4 book.Book json.NewDecoder(resp.Body).Decode(&actual4.ID) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, "", actual4.Title) assert.Equal(t, "", actual4.Author) assert.Equal(t, 0, actual4.Rating) } |
ヘルパー関数
チェック処理が重複しているので、ヘルパー関数を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
package main import ( "bytes" "encoding/json" "fiber-app/book" "fiber-app/repository" "fmt" "net/http" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) func Test_BOOK_HTTP_REQUEST(t *testing.T) { app := fiber.New() initDatabase() defer repository.DBConn.Close() setupRoutes(app) expectBody := book.Book{ Title: "UnitTest Post", Author: "selfnote", Rating: 3, } jsonBody, _ := json.Marshal(&expectBody) readerBody := bytes.NewBuffer(jsonBody) // POST req, _ := http.NewRequest(http.MethodPost, "/api/v1/book", readerBody) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) var actual book.Book json.NewDecoder(resp.Body).Decode(&actual) checkError(t, err, 200, resp.StatusCode, expectBody, actual) // Get - GetBook url := fmt.Sprintf("/api/v1/book/%d", actual.ID) req, _ = http.NewRequest(http.MethodGet, url, nil) resp, err = app.Test(req) var actual2 book.Book json.NewDecoder(resp.Body).Decode(&actual2) checkError(t, err, 200, resp.StatusCode, expectBody, actual2) // Get - GetBooks req, _ = http.NewRequest(http.MethodGet, "/api/v1/books", nil) resp, err = app.Test(req) var actual3 []book.Book json.NewDecoder(resp.Body).Decode(&actual3) checkError(t, err, 200, resp.StatusCode, expectBody, actual3[0]) // DELETE req, _ = http.NewRequest(http.MethodDelete, url, nil) resp, err = app.Test(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) req, _ = http.NewRequest(http.MethodGet, url, nil) resp, err = app.Test(req) var actual4 book.Book json.NewDecoder(resp.Body).Decode(&actual4.ID) expected := book.Book{} checkError(t, err, 200, resp.StatusCode, expected, actual4) } func checkError( t *testing.T, err error, expectedStatus int, actualStatus int, expected, actual book.Book) { assert.Nil(t, err) assert.Equal(t, expectedStatus, actualStatus) assert.Equal(t, expected.Title, actual.Title) assert.Equal(t, expected.Author, actual.Author) assert.Equal(t, expected.Rating, actual.Rating) } |
重複処理を削除するだけで、だいぶスッキリしましたね^^
1 2 3 4 5 6 7 |
$ go test -v 2022/03/06 08:07:14 Database connection successfully opend! 2022/03/06 08:07:14 Database Migrated === RUN Test_BOOK_HTTP_REQUEST --- PASS: Test_BOOK_HTTP_REQUEST (0.01s) PASS ok fiber-app 0.274s |
t.Run
testingパッケージのRunメソッドを使って、テストをグルーピングすることもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
package main import ( "bytes" "encoding/json" "fiber-app/book" "fiber-app/repository" "fmt" "net/http" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) func Test_BOOK_HTTP_REQUEST(t *testing.T) { app := fiber.New() initDatabase() defer repository.DBConn.Close() setupRoutes(app) expectBody := book.Book{ Title: "UnitTest Post", Author: "selfnote", Rating: 3, } jsonBody, _ := json.Marshal(&expectBody) readerBody := bytes.NewBuffer(jsonBody) var id uint t.Run("POST", func(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, "/api/v1/book", readerBody) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) var actual book.Book json.NewDecoder(resp.Body).Decode(&actual) checkError(t, err, 200, resp.StatusCode, expectBody, actual) // ID取得 id = actual.ID }) url := fmt.Sprintf("/api/v1/book/%d", id) t.Run("GET", func(t *testing.T) { // GetBook req, _ := http.NewRequest(http.MethodGet, url, nil) resp, err := app.Test(req) var actual2 book.Book json.NewDecoder(resp.Body).Decode(&actual2) checkError(t, err, 200, resp.StatusCode, expectBody, actual2) // GetBooks req, _ = http.NewRequest(http.MethodGet, "/api/v1/books", nil) resp, err = app.Test(req) var actual3 []book.Book json.NewDecoder(resp.Body).Decode(&actual3) checkError(t, err, 200, resp.StatusCode, expectBody, actual3[0]) }) t.Run("DELETE", func(t *testing.T) { req, _ := http.NewRequest(http.MethodDelete, url, nil) resp, err := app.Test(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) req, _ = http.NewRequest(http.MethodGet, url, nil) resp, err = app.Test(req) var actual4 book.Book json.NewDecoder(resp.Body).Decode(&actual4.ID) expected := book.Book{} checkError(t, err, 200, resp.StatusCode, expected, actual4) }) } func checkError( t *testing.T, err error, expectedStatus int, actualStatus int, expected, actual book.Book) { assert.Nil(t, err) assert.Equal(t, expectedStatus, actualStatus) assert.Equal(t, expected.Title, actual.Title) assert.Equal(t, expected.Author, actual.Author) assert.Equal(t, expected.Rating, actual.Rating) } |
こちらの方がわかりやすいかもしれませんね。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ go test -v === RUN Test_BOOK_HTTP_REQUEST 2022/03/07 05:49:27 Database connection successfully opend! 2022/03/07 05:49:27 Database Migrated === RUN Test_BOOK_HTTP_REQUEST/POST === RUN Test_BOOK_HTTP_REQUEST/GET === RUN Test_BOOK_HTTP_REQUEST/DELETE --- PASS: Test_BOOK_HTTP_REQUEST (0.01s) --- PASS: Test_BOOK_HTTP_REQUEST/POST (0.00s) --- PASS: Test_BOOK_HTTP_REQUEST/GET (0.00s) --- PASS: Test_BOOK_HTTP_REQUEST/DELETE (0.00s) PASS ok fiber-app 0.656s |
Test Driven Tests
更に発展させると…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
package main import ( "bytes" "encoding/json" "fiber-app/book" "fiber-app/repository" "fmt" "net/http" "strings" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) type TestCase struct { testCase string url string method string multiple bool expected book.Book } func Test_BOOK_HTTP_REQUEST(t *testing.T) { app := fiber.New() initDatabase() defer repository.DBConn.Close() setupRoutes(app) expectBody := book.Book{ Title: "UnitTest Post", Author: "selfnote", Rating: 3, } jsonBody, _ := json.Marshal(&expectBody) readerBody := bytes.NewBuffer(jsonBody) var id uint testCases := []TestCase{ {"POST - Book登録テスト", "/api/v1/book", http.MethodPost, false, expectBody}, {"GET - Book1件取得テスト", "/api/v1/book/%d", http.MethodGet, false, expectBody}, {"GET - Book全件取得テスト", "/api/v1/books", http.MethodGet, true, expectBody}, {"DELETE - Book削除テスト", "/api/v1/book/%d", http.MethodDelete, false, expectBody}, {"GET - Book削除確認テスト", "/api/v1/book/%d", http.MethodGet, false, book.Book{}}, } for _, tc := range testCases { t.Run(tc.testCase, func(t *testing.T) { if strings.Contains(tc.url, "%") { tc.url = fmt.Sprintf(tc.url, id) } req, _ := http.NewRequest(tc.method, tc.url, readerBody) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) if tc.method == "DELETE" { // DELETEの場合はチェックを飛ばして、GETで確かめる return } // 全件取得の時はスライス if tc.multiple { var actual []book.Book json.NewDecoder(resp.Body).Decode(&actual) checkError(t, err, 200, resp.StatusCode, tc.expected, actual[0]) } else { var actual book.Book json.NewDecoder(resp.Body).Decode(&actual) checkError(t, err, 200, resp.StatusCode, tc.expected, actual) id = actual.ID } }) } } func checkError( t *testing.T, err error, expectedStatus int, actualStatus int, expected, actual book.Book) { assert.Nil(t, err) assert.Equal(t, expectedStatus, actualStatus) assert.Equal(t, expected.Title, actual.Title) assert.Equal(t, expected.Author, actual.Author) assert.Equal(t, expected.Rating, actual.Rating) } |
こんな感じで実装することもできました。TestCase Structにテストケースのパラメーターを追加していくイメージですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ go test -v === RUN Test_BOOK_HTTP_REQUEST 2022/03/07 05:43:40 Database connection successfully opend! 2022/03/07 05:43:40 Database Migrated === RUN Test_BOOK_HTTP_REQUEST/POST_-_Book登録テスト === RUN Test_BOOK_HTTP_REQUEST/GET_-_Book1件取得テスト === RUN Test_BOOK_HTTP_REQUEST/GET_-_Book全件取得テスト === RUN Test_BOOK_HTTP_REQUEST/DELETE_-_Book削除テスト === RUN Test_BOOK_HTTP_REQUEST/GET_-_Book削除確認テスト --- PASS: Test_BOOK_HTTP_REQUEST (0.01s) --- PASS: Test_BOOK_HTTP_REQUEST/POST_-_Book登録テスト (0.00s) --- PASS: Test_BOOK_HTTP_REQUEST/GET_-_Book1件取得テスト (0.00s) --- PASS: Test_BOOK_HTTP_REQUEST/GET_-_Book全件取得テスト (0.00s) --- PASS: Test_BOOK_HTTP_REQUEST/DELETE_-_Book削除テスト (0.00s) --- PASS: Test_BOOK_HTTP_REQUEST/GET_-_Book削除確認テスト (0.00s) PASS ok fiber-app 0.504s |
おわりに
いかがだったでしょうか。
意外にFiberのテスト記事がないので、この記事がお役に立てば幸いです(@_@)
Go言語でのテストコードの書き方は次の記事が参考になると思いますので、良かったら一読してください。
Go言語まとめ

コメントを残す
コメントを投稿するにはログインしてください。