こんにちは。KOUKIです。
とあるWeb系企業でGoエンジニアをしています。
Golangを採用する理由の一つが、とても早いAPIを実装できることにあります。
しかし、テストが意外に難しいので、今回はAPIのテストについて記述したいと思います。
Webフレームワークは、大人気のginを使いましょう。

事前準備
1 2 3 4 5 6 7 8 |
mkdir gin-test cd gin-test touch main.go touch main_test.go go mod init gin-test go get -u github.com/gin-gonic/gin go get github.com/stretchr/testify |
APIの実装
簡単なAPIを実装します。
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 |
package main import ( "log" "net/http" "github.com/gin-gonic/gin" ) func HomeHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "hello world", }) } func SetupServer() *gin.Engine { r := gin.Default() r.GET("/", HomeHandler) return r } func main() { log.Println("Server Start...") SetupServer().Run() } |
「http://localhost:8080/」にアクセスすると{“message”: “hello world”}を返します。
terminalを用意していただき、下記のコマンドでAPIを起動しましょう。
1 2 3 4 5 6 7 8 9 10 11 |
$ go run main.go 2021/09/07 09:04:30 Server Start... [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> main.HomeHandler (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 |
確認のために、別のterminalでCURLを打ってみます。
1 2 |
$ curl -X GET http://localhost:8080/ {"message":"hello world"} |
OKですね。
APIのテスト
続いて、APIのテストコードを実装します。
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 |
package main import ( "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestHomeHandler(t *testing.T) { mockUserResp := `{"message":"hello world"}` // テスト用のサーバーを立てる ts := httptest.NewServer(SetupServer()) defer ts.Close() // リクエストを送れるか? resp, err := http.Get(fmt.Sprintf("%s/", ts.URL)) if err != nil { t.Fatalf("Expected no error, got %v", err) } defer resp.Body.Close() // Statusコードは200か? if resp.StatusCode != http.StatusOK { t.Fatalf("Expected status code 200, got %v", resp.StatusCode) } responseData, _ := ioutil.ReadAll(resp.Body) if string(responseData) != mockUserResp { t.Fatalf("Expected hello world message, got %v", responseData) } } |
httptestパッケージのNewServer関数で、テスト用のサーバーを起動することができます。
上記では、「http://localhost:8080/」にリクエストを送信し、{“message”: “hello world”}が返却されるかテストしています。
下記のコマンドで、テストを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 |
$ go test [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> gin-test.HomeHandler (3 handlers) [GIN] 2021/09/07 - 09:14:33 | 200 | 72.787µs | 127.0.0.1 | GET "/" PASS ok gin-test 0.635s |
「ok」が返却されたので、テストは成功です^^
testifyを使う
テストは成功しましたが、少しコードが読みにくいですよね。
サードパーティモジュールのtestifyで、テストコードを読みやすくしましょう。
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 |
``` package main import ( "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestHomeHandler(t *testing.T) { mockUserResp := `{"message":"hello world"}` // テスト用のサーバーを立てる ts := httptest.NewServer(SetupServer()) defer ts.Close() // リクエストを送れるか? resp, err := http.Get(fmt.Sprintf("%s/", ts.URL)) defer resp.Body.Close() assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) responseData, _ := ioutil.ReadAll(resp.Body) assert.Equal(t, mockUserResp, string(responseData)) } |
だいぶ読みやすくなりましたね。テストを実行すると先ほどと同様の結果が得られます。
1 2 3 4 5 6 7 8 9 10 11 |
$ go test [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET / --> gin-test.HomeHandler (3 handlers) [GIN] 2021/09/07 - 09:19:46 | 200 | 90.194µs | 127.0.0.1 | GET "/" PASS ok gin-test 0.750s |
Routerの設定
Ginのrouterを設定して、テストする方法もあります。
APIの更新
main.goのAPIをupgradeします。
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 ( "log" "net/http" "github.com/gin-gonic/gin" ) type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` } func MoviesHandler(c *gin.Context) { movies := []Movie{ {1, "movie1", "movie1"}, {2, "movie2", "movie2"}, {3, "movie3", "movie3"}, } c.JSON(http.StatusOK, movies) } func SetupServer() *gin.Engine { r := gin.Default() r.GET("/movies", MoviesHandler) return r } func main() { log.Println("Server Start...") SetupServer().Run() } |
1 2 3 4 |
go run main.go $ curl -X GET http://localhost:8080/movies [{"id":1,"title":"movie1","description":"movie1"},{"id":2,"title":"movie2","description":"movie2"},{"id":3,"title":"movie3","description":"movie3"}] |
テスト
テストコードを実装します。
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 |
package main import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func SetupRouter() *gin.Engine { router := gin.Default() return router } func TestMoviesHandler(t *testing.T) { // routerを設定 r := SetupRouter() r.GET("/movies", MoviesHandler) // リクエストを作成 req, _ := http.NewRequest("GET", "/movies", nil) // テスト用サーバーを作成 w := httptest.NewRecorder() r.ServeHTTP(w, req) var movies []Movie json.Unmarshal([]byte(w.Body.String()), &movies) // assert assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, 3, len(movies)) } |
httpパッケージのGETからGin RouterのGETに変更しました。こっちの方がとっつきやすさがありますよね。Webフレームワークを使うメリットの一つです^^
1 2 3 4 5 6 7 8 9 10 11 |
$ go test [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /movies --> gin-test.MoviesHandler (3 handlers) [GIN] 2021/09/08 - 09:05:05 | 200 | 101.87µs | | GET "/movies" PASS ok gin-test 0.463s |
POSTのテスト
これまでは、GETのテストを見てきましたが、POSTのテストも確認しましょう。
APIの更新
POSTハンドラーを追加します。
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 |
package main import ( "log" "net/http" "github.com/gin-gonic/gin" ) type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` } var movies []Movie func MoviesHandler(c *gin.Context) { c.JSON(http.StatusOK, movies) } func AddMovieHandler(c *gin.Context) { var movie Movie err := c.BindJSON(&movie) if err != nil { c.JSON(http.StatusInternalServerError, "error") } movies = append(movies, movie) } func SetupServer() *gin.Engine { r := gin.Default() r.GET("/movies", MoviesHandler) r.POST("/addmovie", AddMovieHandler) return r } func main() { log.Println("Server Start...") SetupServer().Run() } |
新しく追加したAddMovieHandlerは、以下のように使います。
1 2 3 4 |
go run main.go $ curl -X POST -H "Content-Type: application/json" -d '{"id":1,"title":"movie1","description":"movie1"}' localhost:8080/addmovie $ curl -X GET http://localhost:8080/movies [{"id":1,"title":"movie1","description":"movie1"}] |
テストの追加
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 |
package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func SetupRouter() *gin.Engine { router := gin.Default() return router } .. func TestAddMovieHandler(t *testing.T) { r := SetupRouter() r.POST("/addmovie", AddMovieHandler) movie := Movie{ ID: 1, Title: "Movie", Description: "Movie is wonderful", } jsonValue, _ := json.Marshal(movie) req, _ := http.NewRequest("POST", "/addmovie", bytes.NewBuffer(jsonValue)) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } |
POSTの場合は、JSONデータを先に作ってリクエストと共に渡すようですね。
1 2 3 4 5 6 7 8 9 10 11 |
$ go test ... [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] POST /addmovie --> gin-test.AddMovieHandler (3 handlers) [GIN] 2021/09/08 - 09:24:25 | 200 | 100.838µs | | POST "/addmovie" PASS ok gin-test 0.600s |
カバレッジを出す方法
次のコマンドで、テストカバレッジファイルを出力することができます。
1 2 3 4 5 6 7 8 9 |
$ go test -v -coverprofile=coverage.out ./... $ tree . ├── coverage.out <<<< ├── go.mod ├── go.sum ├── main.go └── main_test.go |
ファイルの中身は下記のようになっています。
1 2 3 4 5 6 7 |
mode: set gin-test/main.go:18.36,20.2 1 1 gin-test/main.go:22.38,25.16 3 1 gin-test/main.go:28.2,28.32 1 1 gin-test/main.go:25.16,27.3 1 0 gin-test/main.go:31.32,36.2 4 0 gin-test/main.go:38.13,41.2 2 0 |
このままじゃよくわからないと思うので、HTMLとして画面に表示させましょう。
1 |
go tool cover -html=coverage.out |
上記のコマンドを実行すると、ブラウザが自動で立ち上がります。

テストでカバーされている箇所は緑、されていない箇所は赤ですね。この赤の部分をなるべく少なくできるとカバレッジが上がるのだと思います。
まとめ
簡単でしたが、以上です。
httptestパッケージのNewServer関数を使えばテスト用のサーバーを起動でき、実装したAPIへリクエストを送信する処理を実装し、testifyで結果の評価を行う。
APIのテストはこのような流れになると思います。この基本的な流れを組めば、あとは応用でなんとかなります。
テストコードをたくさん書いて、慣れていきましょう^^
それでは、また!
Go記事まとめ
ソースコード
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 |
package main import ( "fmt" ) type myArray []int func mergeSortedArrays(array1, array2 myArray) myArray { mergeArray := []int{} smallArray := []int{} largeArray := []int{} largeArrayItem := 0 smallArrayMap := make(map[int]int) smallArrayItem := 0 // 配列が大きさ判定 if len(array1) > len(array2) { smallArray = array2 largeArray = array1 } else { smallArray = array1 largeArray = array2 } // 配列大 -> 小でループ for _, b := range largeArray { for _, s := range smallArray { // 既に追加したyの値は追加したくないのでスキップ if _, ok := smallArrayMap[s]; ok { continue } // 配列の大小チェック if b < s { mergeArray = append(mergeArray, b) smallArrayItem = s break } else if b == s { mergeArray = append(mergeArray, b) break } else { largeArrayItem = b mergeArray = append(mergeArray, s) smallArrayMap[s] = s // 追加したyの値を記録 } } } // 最後に大きい方の数字を入れる if largeArrayItem > smallArrayItem { mergeArray = append(mergeArray, largeArrayItem) } else { mergeArray = append(mergeArray, smallArrayItem) } return mergeArray } func main() { array1 := myArray{0, 3, 4, 41} array2 := myArray{4, 6, 30} fmt.Println( "array1 - array2: ", mergeSortedArrays(array1, array2)) array3 := myArray{3, 9, 34} array4 := myArray{8, 9, 20, 33} fmt.Println( "array3 - array4: ", mergeSortedArrays(array3, array4)) array5 := myArray{1, 3, 6} array6 := myArray{2, 4, 5, 7} fmt.Println( "array5 - array6: ", mergeSortedArrays(array5, array6)) } |
コメントを残す
コメントを投稿するにはログインしてください。