はじめに
最初に少し自己紹介と、この記事を書くまでの経緯から説明します!
2024年4月から新卒としてディップ株式会社に入社し、主にGoを使ったバックエンド開発をしている@syun990806と申します。
そして今回ディップのアドベントカレンダーに参加し、人生初めての記事を書かせて頂きます!
プログラミング自体は大学生の頃から少し触れていましたが、エンジニアという正式なキャリアとしてはまだまだ1年目、未熟ながら日々勉強しつつ頑張っています💪
そして現在進行中のタスクとして、所属チームが持つプロダクトのテスト強化を行なっています。
そこでリファクタリングをした際に考えたこと・学んだことなどを備忘録的に綴ろうと思います💭
どんなことに取り組んだのか
先に説明した通り、「既存プロダクトのテスト強化」に取り組みました。
具体的には、「テストケースを増やし、カバレッジ(テストの網羅率)を向上させること」がこのタスクのゴールです。
そのため、テストケースを増やせばすぐにカバレッジは上げることができます。
ただし今回の大きな目的としては「テスト強化」であり、「保守しやすいテストコードへのリファクタリング」もタスクに含まれた目的だと考え、「保守しやすいコードにリファクタしながら、カバレッジを向上させること」を最終的なゴールとしました。
どんな課題があったか
まずは既存のテストコードで網羅されていない部分として、以下2点の異常系テストケースが存在していませんでした。
- リクエスト時の
400 - Bad Request
系のエラー検証- 解決策:リクエストボディのバリデーションに引っかかるテストケースを追加する
- DB接続時の
500 - Internal Server Error
- 解決策:対象部分をモック化し、接続エラーを返すテストケースを追加する
これらは上記の解決策で実装しましたが、よくあるテストケースだと思われるため、本記事では割愛します。
そしてもう一つ、関連issueとして以下のようなものがありましたが、どう解釈すべきか少し迷った部分がありました。
- 送信したリクエストをもとに、登録されたレコードの内容が想定通りであるかを検証するテストケースの追加
こちらはissueが立てられてからだいぶ日が経っていたため、issue詳細について分かる方がチームにいませんでした。
そのため自分で解釈を検討してチームに提案し、メンバー内で以下のように定義し直して実装を進めました。
- 「送信したリクエスト」→「テストケースごとに設けたリクエストボディ」
- 「登録されたレコード」→「DB登録時のレコードの値」
今回はこちらの実装内容について説明します。
実装するにあたっての問題点
自分なりの解釈は定義できたものの、ソースコードを見ると、どうやら単純にテストケースを追加すれば良いというものではなさそうでした。
まずは元々実装されていたテストケースの構造について説明します。
"正常系_新規登録パターン": {
request: func() string {
resp, err := http.Post(
server.URL,
"application/json",
strings.NewReader(`{
(リクエストボディ内容)
}`),
)
if err != nil {
t.Error(err)
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
return string(buf.String())
}
}
ざっくりとこのような構造で正常系・異常系のテストケースが並んでいました。
次にこれらテストケースを処理・実行している部分です。
t.Run(tn, func(t *testing.T) {
(前処理実行)
defer (後処理実行)
response := tt.request()
assert.Contains(t, response, tt.wantResponse, tn)
})
以上のようなテストケースと、その実行部分という構造になっていました。
この実装が表すのは、
「1つ1つのテストケース内で、データ定義から処理までが完結されている実装」
ということです。
つまりこのままの実装では、実際に返却されるレスポンス("ステータスコード"という"結果")を検証することしかできない状態になっています。
あくまでも今回は、"結果"ではなく一連の処理の中で行われる"過程"を検証したいので、テストケースの構造から見直す必要がありました。
しかし最初はどうしたら良いのか全く分かりませんでした。
そこで周りの先輩に聞くなどして得たヒントが、「テーブル駆動なテストケースにする」というものでした。
テーブル駆動なテストケースとは
そもそも「テーブル駆動なテストケース」が、どういうものを表しているのか分からなかったため調べたところ、こちらの記事がとても参考になりました。
こちらの記事の結論でも書かれているように、
「テーブルとテストの部分でデータとロジックを分離する」
これに尽きます。
実際に行った改修
改修にあたってすべきことは、
「テストケース内に組み込まれている処理(ロジック)部分を切り離す」
ことです。
もう少し具体的に言うと、現時点ではリクエストを関数型として、最終的に文字列のステータスコードを返却するような実装になっていますが、1つ1つのテストケースにはリクエストボディ(データ)のみが定義されている状態にすることを目指します。
そこでまず、テストケースの構造を以下のようにリファクタしました。
Query: types.NewRecordToAPI{
(リクエストボディ内容)
}
ここでのQuery
は、各テストケースごとのリクエストボディ内容を構造体で定義しています。
次にこれらテストケースを処理・実行している部分です。
t.Run(tn, func(t *testing.T) {
(前処理実行)
defer (後処理実行)
(中略)
// 送信したリクエストをもとに、登録されたレコードの内容が想定通りであるかの検証
responseData := &types.NewRecordToAPI{} // 必須となるリクエストボディが定義された構造体
err = db.QueryRow((新規登録のSQL文), tt.(QueryのID)).Scan(
&responseData.(IDを持つカラム名),
&responseData.(その他定義されたカラム名),
...
)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(tt.Query, *responseData) {
t.Errorf("Send request: %v, Actual response data: %v", tt.Query, *responseData)
}
(中略)
)
このようにテストケースとしては、
「リクエストボディを表す構造体」という"データ"のみを定義する形にし、
実際にテストケースを実行している部分においては、
「リクエストボディを表す構造体と、DB登録時に返却される値を構造体定義したものと比較」
することによって、今回の問題を解決しました。
今回の学びと今後の課題
Goの書き方やテストについてたくさん学ぶことができたのはもちろんのこと、
「抽象的な課題・問題に対して、それをどう自分で解釈して具体化するか」という一連の流れを経験できたことが、今回の一番の収穫だったと感じています
また結果としても、元々50%程度だったテストカバレッジを100%にできました。
が、実はこれはテスト対象となっている8ファイルのうちの1つなので、まだまだ道のりは長いなと...
なんにせよ、最終的なゴールまでの課題を1つクリアできたので、とても達成感があり嬉しかったです!
さいごに
今回のタスクを行うにあたり、たくさんアドバイスを下さった先輩方みなさん、本当にありがとうございました🙇
まだまだ未熟極まりない自分ですが、温かく見守って頂けると嬉しいです。
これからもよろしくお願いいたします!