Featured image of post GoでサイコロAPIを作る

GoでサイコロAPIを作る

Web APIつくってみる

Web APIを自分で最初から作ったことがなかったので, Goの練習も兼ねてやってみた.


やったことのまとめ

以下のようにランダムなサイコロの出目(number)とサイコロの面の数(faces)をJSONで返すだけのWeb APIをGoで作った.

# 普通に叩くと6面サイコロを振る
$ curl -X GET "localhost:8080"
{"number":1,"faces":6}
$ curl -X GET "localhost:8080"
{"number":5,"faces":6}

# クエリパラメータで面の数を指定できる
$ curl -X GET "localhost:8080?faces=100"
{"number":71,"faces":100}

# ズルをするためのエンドポイントも用意
$ curl -X GET "localhost:8080/cheat"
{"number":6,"faces":6}
$ curl -X GET "localhost:8080/cheat?faces=100&number=99"
{"number":99,"faces":100}

つかうもの

  • macOS Mojave 10.14
  • anyenv 1.1.1
    • インストール済み
  • goenv 2.0.0beta11
    • インストール済み
  • go version go1.14.6 darwin/amd64
    • 今回入れる

やったこと

準備

まずは開発環境の準備.

公式によると現在(2020/07/16)Goの最新版が 1.14.6 なので, これを使うようにする.

# goenvの更新
$ anyenv install goenv
anyenv: /Users/uzimihsr/.anyenv/envs/goenv already exists
Reinstallation keeps versions directories
continue with installation? (y/N) y
...

Install goenv succeeded!
Please reload your profile (exec $SHELL -l) or open a new session.
$ exec $SHELL -l

# Go 1.14.6のインストール
$ cd
$ goenv install 1.14.6
$ goenv global 1.14.6
$ goenv rehash
$ exec $SHELL -l

# 確認
$ goenv version
1.14.6 (set by /Users/uzimihsr/.anyenv/envs/goenv/version)
$ go version
go version go1.14.6 darwin/amd64
$ echo $GOPATH
/Users/uzimihsr/go/1.14.6

次にGitHubリポジトリを新規作成する.
GitHub

作ったリポジトリ : https://github.com/uzimihsr/dice-api

作成したリポジトリをローカルにcloneして, Go Modulesとか.gitignoreの準備をする.
Go Modulesの使い方は公式を参考にする.
.gitignoregitignore.ioを使って作るのが楽.

# 適当なディレクトリで作業
$ cd workspace
$ git clone https://github.com/uzimihsr/dice-api.git
Cloning into 'dice-api'...
warning: You appear to have cloned an empty repository.
$ cd dice-api

# Go Moduleの初期化
$ go mod init github.com/uzimihsr/dice-api
go: creating new go.mod: module github.com/uzimihsr/dice-api
$ ls
go.mod

# gitignore.ioのAPIを利用して.gitignoreを作成する
$ curl -o ./.gitignore https://www.toptal.com/developers/gitignore/api/go,macos,linux

# いったんpushしておく
$ echo "# dice-api" > README.md
$ git add .
$ git commit -m "first commit"
$ git push origin master

このときのリポジトリはこんなかんじ.

サイコロの作成

まずはサイコロの動作を作ってみる.
以前作ったものを流用する.

$ mkdir dice && cd dice
$ vim dice.go

今回は普通にサイコロの出目を返すメソッドRoll()の他に指定した出目を返すCheat()を作ってみた.

package dice
import (
"math/rand"
"time"
)
type DiceInterface interface {
Roll() int
Cheat(int) int
GetFaces() int
SetFaces(int)
}
type Dice struct {
Faces int
}
func (d *Dice) GetFaces() int {
return d.Faces
}
func (d *Dice) SetFaces(faces int) {
d.Faces = faces
}
func (d *Dice) Roll() int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(d.Faces) + 1
}
func (d *Dice) Cheat(n int) int {
if n <= 0 || n >= d.Faces {
n = d.Faces
}
return n
}
func NewDice(faces int) *Dice {
if faces <= 0 {
faces = 6
}
d := new(Dice)
d.Faces = faces
return d
}
view raw dice.go hosted with ❤ by GitHub

ためしに動かしてみる.

$ cd ..
$ vim main.go
$ go run main.go
6
5
$ go run main.go
1
5
package main
import (
"fmt"
"github.com/uzimihsr/dice-api/dice"
)
func main() {
d := dice.NewDice(6)
number := d.Roll()
fmt.Println(number)
cheatNumber := d.Cheat(5)
fmt.Println(cheatNumber)
}
view raw main.go hosted with ❤ by GitHub

いい感じ.

HTTPハンドラの作成

次にこれをWeb APIとして動かすためのハンドラ関数を作る.

$ mkdir handler && cd handler
$ vim handler.go

ポイントはハンドラ関数DiceHandler(), CheatDiceHandler()の引数でinterfaceを受けるようにしているところ.
こうしておくと後でテストを書くときにmockを使いやすくなる.

package handler
import (
"encoding/json"
"net/http"
"strconv"
"github.com/uzimihsr/dice-api/dice"
)
type diceResult struct {
Number int `json:"number"`
Faces int `json:"faces"`
}
func DiceHandler(d dice.DiceInterface) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = diceGet(w, r, d)
default:
diceNotFound(w)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func CheatDiceHandler(d dice.DiceInterface) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = cheatDiceGet(w, r, d)
default:
diceNotFound(w)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func diceGet(w http.ResponseWriter, r *http.Request, d dice.DiceInterface) (err error) {
faces := 6
q := r.URL.Query()
queryFaces := q.Get("faces")
if queryFaces != "" {
faces, err = strconv.Atoi(queryFaces)
if err != nil {
return err
}
if faces <= 0 {
faces = 6
}
}
d.SetFaces(faces)
number := d.Roll()
result := diceResult{number, d.GetFaces()}
j, err := json.Marshal(result)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
return
}
func cheatDiceGet(w http.ResponseWriter, r *http.Request, d dice.DiceInterface) (err error) {
faces := 6
q := r.URL.Query()
queryFaces := q.Get("faces")
queryNumber := q.Get("number")
if queryFaces != "" {
faces, err = strconv.Atoi(queryFaces)
if err != nil {
return err
}
if faces <= 0 {
faces = 6
}
}
num := faces
if queryNumber != "" {
num, err = strconv.Atoi(queryNumber)
if err != nil {
return err
}
}
d.SetFaces(faces)
number := d.Cheat(num)
result := diceResult{number, d.GetFaces()}
j, err := json.Marshal(result)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
return
}
func diceNotFound(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"message": "not found"}`))
}
view raw handler.go hosted with ❤ by GitHub

これらのハンドラ関数を扱ってHTTPサーバーを動かすため, main.goを修正する.

$ cd ..
$ vim main.go
$ go run main.go
# 動作を確認したら Ctrl+C で終了

こちらはhttp.NewServeMux()を使ってリクエストパスごとに異なるハンドラを呼び出すようにしているのがポイント.

package main
import (
"net/http"
"github.com/uzimihsr/dice-api/dice"
"github.com/uzimihsr/dice-api/handler"
)
func main() {
d := dice.NewDice(6)
mux := http.NewServeMux()
mux.HandleFunc("/", handler.DiceHandler(d))
mux.HandleFunc("/cheat", handler.CheatDiceHandler(d))
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
server.ListenAndServe()
}
view raw main.go hosted with ❤ by GitHub

main.goを実行するとMacの警告が出るので, 許可を選択するとアプリが動く.
許可する

試しに別のターミナルからAPIを叩いてみる.

# 正常系
$ curl "localhost:8080"
{"number":1,"faces":6}
$ curl "localhost:8080?faces=12"
{"number":10,"faces":12}
$ curl "localhost:8080/cheat"
{"number":6,"faces":6}
$ curl "localhost:8080/cheat?number=1"
{"number":1,"faces":6}
$ curl "localhost:8080/cheat?number=1&faces=18"
{"number":1,"faces":18}

# 異常系
$ curl -i -X POST "localhost:8080"
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Thu, 23 Jul 2020 08:12:39 GMT
Content-Length: 24

{"message": "not found"}
$ curl -i -X GET "localhost:8080?faces=hoge"
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Thu, 23 Jul 2020 08:13:04 GMT
Content-Length: 45

strconv.Atoi: parsing "hoge": invalid syntax
$ curl -i -X POST "localhost:8080/cheat"
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Thu, 23 Jul 2020 08:13:58 GMT
Content-Length: 24

{"message": "not found"}
$ curl -i -X GET "localhost:8080/cheat?number=hoge"
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Thu, 23 Jul 2020 08:14:12 GMT
Content-Length: 45

strconv.Atoi: parsing "hoge": invalid syntax

サイコロの出目(number)と面の数(faces)がJSONで返ってきて,
異常なクエリパラメータやHTTPメソッドでリクエストした場合には指定したステータスコード(404, 500)が返ってくることが確認できた.
やったぜ.

このときのリポジトリはこんなかんじ.

テストの作成

作りたいものは作れたのでここで終わってもいいけど, せっかくなのでテストコードを書いてみる.

まずはdiceパッケージから.

$ cd ./dice
$ vim dice_test.go

カバレッジ100%になるように書いたけど, 無駄なテストも含まれている.

package dice
import (
"fmt"
"testing"
)
func TestSetFaces(t *testing.T) {
faces := 6
d := Dice{}
d.SetFaces(faces)
expect := faces
actual := d.Faces
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
func TestGetFaces(t *testing.T) {
faces := 6
d := Dice{}
d.Faces = faces
expect := faces
actual := d.GetFaces()
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// Dice型が作成されることのテスト
func TestNewDice01(t *testing.T) {
d := NewDice(6)
dice := new(Dice)
expect := fmt.Sprintf("%T", dice)
actual := fmt.Sprintf("%T", d)
if expect != actual {
t.Errorf("expected: %s, actual: %s", expect, actual)
}
}
// facesで指定した値がFacesになることのテスト
func TestNewDice02(t *testing.T) {
faces := 12
d := NewDice(faces)
expect := faces
actual := d.Faces
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// facesで0以下の値を指定した場合, Facesが6になることのテスト
func TestNewDice03(t *testing.T) {
faces := -12
d := NewDice(faces)
expect := 6
actual := d.Faces
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// Roll結果が0以上faces以下になることのテスト
func TestRoll01(t *testing.T) {
faces := 6
d := NewDice(faces)
number := d.Roll()
if number > faces || number <= 0 {
t.Errorf("invalid roll result: %d", number)
}
}
// Cheat結果がnumberと等しくなることのテスト
func TestCheat01(t *testing.T) {
faces := 6
number := 6
d := NewDice(faces)
expect := number
actual := d.Cheat(number)
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// numberで無効な値を指定した場合, Cheat結果がfacesと等しくなることのテスト
func TestCheat02(t *testing.T) {
faces := 6
number := -6
d := NewDice(faces)
d.Cheat(number)
expect := d.Faces
actual := d.Cheat(number)
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
view raw dice_test.go hosted with ❤ by GitHub

次にhandlerパッケージのテストを作る.
handlerパッケージは自作のdiceパッケージに依存しているので, diceパッケージのmockを用意したい.
(今回はDBとかに接続してないのでdiceを直接呼び出してもいいんだけど, サイコロの出目が確率で変わっちゃうのでテストがしづらい)

こんなときにはgomockを使う.
mockしたいinterfaceのファイルを指定するだけでmock用のコードを作成してくれるのでめちゃ便利.

# mockパッケージのインストール
$ cd ..
$ go get github.com/golang/mock/mockgen

# diceのmockを作成
$ mockgen -source ./dice/dice.go -destination ./mock_dice/mock_dice.go
// Code generated by MockGen. DO NOT EDIT.
// Source: ./dice/dice.go
// Package mock_dice is a generated GoMock package.
package mock_dice
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockDiceInterface is a mock of DiceInterface interface
type MockDiceInterface struct {
ctrl *gomock.Controller
recorder *MockDiceInterfaceMockRecorder
}
// MockDiceInterfaceMockRecorder is the mock recorder for MockDiceInterface
type MockDiceInterfaceMockRecorder struct {
mock *MockDiceInterface
}
// NewMockDiceInterface creates a new mock instance
func NewMockDiceInterface(ctrl *gomock.Controller) *MockDiceInterface {
mock := &MockDiceInterface{ctrl: ctrl}
mock.recorder = &MockDiceInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDiceInterface) EXPECT() *MockDiceInterfaceMockRecorder {
return m.recorder
}
// Roll mocks base method
func (m *MockDiceInterface) Roll() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Roll")
ret0, _ := ret[0].(int)
return ret0
}
// Roll indicates an expected call of Roll
func (mr *MockDiceInterfaceMockRecorder) Roll() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Roll", reflect.TypeOf((*MockDiceInterface)(nil).Roll))
}
// Cheat mocks base method
func (m *MockDiceInterface) Cheat(arg0 int) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Cheat", arg0)
ret0, _ := ret[0].(int)
return ret0
}
// Cheat indicates an expected call of Cheat
func (mr *MockDiceInterfaceMockRecorder) Cheat(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cheat", reflect.TypeOf((*MockDiceInterface)(nil).Cheat), arg0)
}
// GetFaces mocks base method
func (m *MockDiceInterface) GetFaces() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFaces")
ret0, _ := ret[0].(int)
return ret0
}
// GetFaces indicates an expected call of GetFaces
func (mr *MockDiceInterfaceMockRecorder) GetFaces() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFaces", reflect.TypeOf((*MockDiceInterface)(nil).GetFaces))
}
// SetFaces mocks base method
func (m *MockDiceInterface) SetFaces(arg0 int) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetFaces", arg0)
}
// SetFaces indicates an expected call of SetFaces
func (mr *MockDiceInterfaceMockRecorder) SetFaces(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFaces", reflect.TypeOf((*MockDiceInterface)(nil).SetFaces), arg0)
}
view raw mock_dice.go hosted with ❤ by GitHub

このmockを使ったテストを書いてみる.

$ cd handler
$ vim handler_test.go

準備したmockを各ハンドラ関数の引数に渡してやることで, 実際のdiceを呼び出すことなくハンドラ関数のテストができるようになる.
ハンドラ関数を作るときにdiceinterfaceを引数で受けるようにしたのはこのため.

package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/uzimihsr/dice-api/mock_dice"
)
// リクエストパスが"/"でクエリパラメータがない場合, faces=6でRollが呼び出されることのテスト
func TestDiceGet01(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(1)
m.EXPECT().Roll().Return(3).Times(1)
m.EXPECT().GetFaces().Return(6).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/", DiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 3
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/"で"faces=12"をクエリパラメータで指定した場合, faces=12でRollが呼び出されることのテスト
func TestDiceGet02(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(12).Times(1)
m.EXPECT().Roll().Return(3).Times(1)
m.EXPECT().GetFaces().Return(12).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/", DiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/?faces=12", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 3
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/"で"faces=-12"をクエリパラメータで指定した場合, faces=6でRollが呼び出されることのテスト
func TestDiceGet03(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(1)
m.EXPECT().Roll().Return(3).Times(1)
m.EXPECT().GetFaces().Return(6).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/", DiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/?faces=-12", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 3
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/"で"faces=asdf"をクエリパラメータで指定した場合, エラーになることのテスト
func TestDiceGet04(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(0)
m.EXPECT().Roll().Return(3).Times(0)
m.EXPECT().GetFaces().Return(6).Times(0)
// do
mux := http.NewServeMux()
mux.HandleFunc("/", DiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/?faces=asdf", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusInternalServerError {
t.Errorf("Response code is %v", writer.Code)
}
}
// リクエストパスが"/cheat"でクエリパラメータがない場合, num=6でCheatが呼び出されることのテスト
func TestCheatDiceGet01(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(1)
m.EXPECT().Cheat(6).Return(6).Times(1)
m.EXPECT().GetFaces().Return(6).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/cheat", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 6
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/cheat"で"number=4"をクエリパラメータで指定した場合, num=4でCheatが呼び出されることのテスト
func TestCheatDiceGet02(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(1)
m.EXPECT().Cheat(4).Return(4).Times(1)
m.EXPECT().GetFaces().Return(4).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/cheat?number=4", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 4
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/cheat"で"faces=12"をクエリパラメータで指定した場合, num=12でCheatが呼び出されることのテスト
func TestCheatDiceGet03(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(12).Times(1)
m.EXPECT().Cheat(12).Return(12).Times(1)
m.EXPECT().GetFaces().Return(12).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/cheat?faces=12", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 12
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/cheat"で"faces=-12"をクエリパラメータで指定した場合, num=6でCheatが呼び出されることのテスト
func TestCheatDiceGet04(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(1)
m.EXPECT().Cheat(6).Return(6).Times(1)
m.EXPECT().GetFaces().Return(6).Times(1)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/cheat?faces=-12", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusOK {
t.Errorf("Response code is %v", writer.Code)
}
result := diceResult{}
json.Unmarshal(writer.Body.Bytes(), &result)
expect := 6
actual := result.Number
if expect != actual {
t.Errorf("expected: %d, actual: %d", expect, actual)
}
}
// リクエストパスが"/cheat"で"faces=asdf"をクエリパラメータで指定した場合, エラーになることのテスト
func TestCheatDiceGet05(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(0)
m.EXPECT().Cheat(6).Return(6).Times(0)
m.EXPECT().GetFaces().Return(6).Times(0)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/cheat?faces=asdf", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusInternalServerError {
t.Errorf("Response code is %v", writer.Code)
}
}
// リクエストパスが"/cheat"で"number=asdf"をクエリパラメータで指定した場合, エラーになることのテスト
func TestCheatDiceGet06(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(0)
m.EXPECT().Cheat(6).Return(6).Times(0)
m.EXPECT().GetFaces().Return(6).Times(0)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/cheat?number=asdf", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusInternalServerError {
t.Errorf("Response code is %v", writer.Code)
}
}
// DiceHandlerが無効なメソッドで呼ばれた場合, diceNotFoundが呼ばれることのテスト
func TestDiceNotFound01(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(0)
m.EXPECT().Roll().Return(3).Times(0)
m.EXPECT().GetFaces().Return(6).Times(0)
// do
mux := http.NewServeMux()
mux.HandleFunc("/", DiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("POST", "/", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusNotFound {
t.Errorf("Response code is %v", writer.Code)
}
}
// CheatDiceHandlerが無効なメソッドで呼ばれた場合, diceNotFoundが呼ばれることのテスト
func TestDiceNotFound02(t *testing.T) {
// mock
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_dice.NewMockDiceInterface(ctrl)
m.EXPECT().SetFaces(6).Times(0)
m.EXPECT().Cheat(6).Return(6).Times(0)
m.EXPECT().GetFaces().Return(6).Times(0)
// do
mux := http.NewServeMux()
mux.HandleFunc("/cheat", CheatDiceHandler(m))
writer := httptest.NewRecorder()
request := httptest.NewRequest("POST", "/cheat", nil)
mux.ServeHTTP(writer, request)
// check
if writer.Code != http.StatusNotFound {
t.Errorf("Response code is %v", writer.Code)
}
}
view raw handler_test.go hosted with ❤ by GitHub

テストコードが作れたので, 実際にテストを実行する.

$ cd ..
# diceパッケージのテスト
$ go test -cover ./dice
ok  	github.com/uzimihsr/dice-api/dice	0.006s	coverage: 100.0% of statements
# handlerパッケージのテスト
$ go test -cover ./handler
ok  	github.com/uzimihsr/dice-api/handler	0.016s	coverage: 100.0% of statements
# 全部まとめてテスト
$ go test -cover ./...
?   	github.com/uzimihsr/dice-api	[no test files]
ok  	github.com/uzimihsr/dice-api/dice	(cached)	coverage: 100.0% of statements
ok  	github.com/uzimihsr/dice-api/handler	(cached)	coverage: 100.0% of statements
?   	github.com/uzimihsr/dice-api/mock_dice	[no test files]

これでテストも(一応)できた.

最終的なディレクトリの構成はこんなかんじ.
テストを実行したのでパッケージの依存関係を管理するためのファイルgo.modgo.sumが修正/追加されている.
こいつらも一緒にGitで管理しておくと, 別の環境でこれをビルドするときに便利だったりする. らしい.

$ tree .
.
├── README.md
├── dice
│   ├── dice.go
│   └── dice_test.go
├── go.mod
├── go.sum
├── handler
│   ├── handler.go
│   └── handler_test.go
├── main.go
└── mock_dice
    └── mock_dice.go

3 directories, 9 files

最終的なリポジトリはこんなかんじ.

おわり

Web APIっぽいものを0から作ってみた. テストまでやったのでけっこうしんどかった.
あとはビルドすれば普通に動くはずなので,
暇があったらKubernetesで動かすところまでやってみたい.

おまけ

参考にしたもの