APIの自動テストとモックAPIを駆使してTDD風に開発する
こんにちは、バックエンドを中心に開発をしている野島と申します。
最近下記の流れで開発をしており、とても開発しやすいと感じているので共有します。
- APIの自動テストの作成
- モックAPIの作成
- APIの処理の実装
TDDは下記の順序で行いますが、それを拡張してAPI開発にあてはめたようなスタイルです。
レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く。
グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
リファクタリング:テストを通すために発生した重複をすべて除去する。
テスト駆動開発 p.ⅹより引用。
それでは、内容に入っていきます。
0. 前提
Go言語でAPI開発し、テストツールにはscenarigoを利用するとします。
scenarigo は YAML でテストシナリオを記述するAPIテストツールです。簡潔に記載できるので、どのようなものか知らずとも本記事を読み進めることができると思います。
お題として、シンプルな「ユーザー情報取得API」を開発するとします。
このAPIは、特定のユーザーIDに基づいてユーザー情報を取得します。
こちらを先ほど紹介した流れで開発していきます。
1. APIの自動テストの作成
まずは、ユーザー情報取得APIのための自動テストを作成します。
idが1のユーザー情報を取得するテストを作成します。
title: ユーザー情報取得
steps:
- title: get
protocol: http
request:
method: GET
url: http://localhost:8080/users/1
expect:
code: OK
body:
id: 1
name: ユーザー名
age: 20
このテストを実行すると、なにも実装していないので当然失敗します。
以降の手順でテストが成功するように実装していきます。
レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く。
$ scenarigo run
--- FAIL: test.yaml (0.01s)
--- FAIL: test.yaml/ユーザー情報取得 (0.01s)
--- FAIL: test.yaml/ユーザー情報取得/get (0.01s)
request:
method: GET
url: http://localhost:8080/users/1
header:
User-Agent:
- scenarigo/v0.16.2
elapsed time: 0.006745 sec
failed to send request: Get "http://localhost:8080/users/1": dial tcp [::1]:8080: connect: connection refused
3 | - title: get
4 | protocol: http
5 | request:
> 6 | method: GET
^
7 | url: http://localhost:8080/users/1
8 | expect:
9 | code: OK
10 |
FAIL
FAIL test.yaml 0.011s
FAIL
この時点でのコミットは下記です。
2. モックAPIの作成
リクエストを受け取り、レスポンスを返すだけのAPIを作成します。
グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
package main
import "net/http"
// title: ユーザー情報取得
// steps:
// - title: get
// protocol: http
// request:
// method: GET
// url: http://localhost:8080/users/1
// expect:
// code: OK
// body:
// id: 1
// name: ユーザー名
// age: 20
func main() {
http.HandleFunc("/users/1", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"id":1,"name":"ユーザー名","age":20}`))
})
http.ListenAndServe(":8080", nil)
}
テストをコメントで貼り付けているのは、Copilot任せに実装するためです。
実際には後からこのコメントはcommitするタイミングで削除します。
この時点でテストが成功するようになります。
$ scenarigo run
ok test.yaml 0.013s
この時点でのコミットは下記です。
3. APIの処理の実装
最後に、ユーザー情報取得APIの処理を実装します。
データーベースからIDに基づいてユーザー情報を取得し、レスポンスを返します。
リファクタリング:テストを通すために発生した重複をすべて除去する。
package main
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
)
func main() {
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
sub := strings.TrimPrefix(r.URL.Path, "/users/")
_, id := filepath.Split(sub)
// id to int
userID, _ := strconv.Atoi(id)
user, _ := GetUser(userID)
response, _ := json.Marshal(user)
w.Write(response)
})
http.ListenAndServe(":8080", nil)
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func GetUser(id int) (User, error) {
user, ok := database[id]
if !ok {
return User{}, fmt.Errorf("id %d is not found", id)
}
return user, nil
}
// 簡易的なデータベース
var database map[int]User = map[int]User{
1: {ID: 1, Name: "ユーザー名", Age: 20},
2: {ID: 2, Name: "Taro", Age: 21},
3: {ID: 3, Name: "Jiro", Age: 22},
// ...
}
$ scenarigo run
ok test.yaml 0.010s
テストはユーザーID1指定のものでした。
ためしに、ユーザーID2のユーザー情報を取得した場合のレスポンスは下記のようになります。
$ curl localhost:8080/users/2
{"id":2,"name":"Taro","age":21}
この時点でのコミットは下記です。
まとめ
API開発において、テスト駆動開発のようなスタイルで開発する方法を紹介しました。
この方法には3種類のメリットがあります。
- 実装の高速化
- テストベースで生成AIに実装を任せることができる
- 自動テストが存在するので実装の正しさの確認をテスト任せにできる
- 開発の連携のしやすさの向上
- テストを先に書くことで、APIの仕様を明確にすることができる
- モックのAPIを先にデプロイすることで、フロントエンドとの結合を早めることができる
- 品質の向上
- テストをCIに組み込むことでデグレを防ぐことができる
私はモックのAPIがある時点でテストも存在することに強力なメリットを感じています。
モックAPIを作成することで、フロントエンドとの結合を早めることができます。自分がもっているタスクがフロントエンドのタスクの進捗をブロッキングすることがなくなり、自身の心理的な負荷が減ります。
また、本当の処理を実装する際にすでにテストが存在することで、APIの動作確認が格段に楽になります。
実際の運用では、ここで作ったテストをすべてCIに組み込むのではなく、よさそうなケースを選んでCIに組み込んでいます。
APIのテストはコード単体で動くUTより遅く不安定さがあり、CIに入れるテストケースは選ぶ必要があるためです。
本記事を読んだ方が、この方法に興味を持っていただけたら幸いです。
参考
Discussion