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リポジトリを新規作成する.
作ったリポジトリ : https://github.com/uzimihsr/dice-api
作成したリポジトリをローカルにcloneして, Go Modules
とか.gitignore
の準備をする.Go Modules
の使い方は公式を参考にする..gitignore
はgitignore.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 | |
} |
ためしに動かしてみる.
$ 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) | |
} |
いい感じ.
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"}`)) | |
} |
これらのハンドラ関数を扱って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() | |
} |
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) | |
} | |
} |
次に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) | |
} |
このmockを使ったテストを書いてみる.
$ cd handler
$ vim handler_test.go
準備したmockを各ハンドラ関数の引数に渡してやることで, 実際のdice
を呼び出すことなくハンドラ関数のテストができるようになる.
ハンドラ関数を作るときにdice
のinterface
を引数で受けるようにしたのはこのため.
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) | |
} | |
} |
テストコードが作れたので, 実際にテストを実行する.
$ 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.mod
とgo.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
で動かすところまでやってみたい.