この記事はウェブクルー Advent Calendar 2023 15日目の記事です。
昨日は@piwi さんの「文章を書く際に心掛けていること」でした。
はじめに
ウェブクルー入社2年目の久保田です。
普段はScalaでバックエンドの開発を行っています。
ここでは最近取り組んでいる単体テストにおいて学んだことを自分への備忘録を兼ねて書いていきます。
概要
最近社内の方に単体テストの考え方/使い方という本をお勧めされ、読んだところ非常に勉強になりました。(半分くらいしか理解できていませんが)
こちらの書籍の詳細をまとめている記事はたくさんあるので、単体テスト初心者なりに今すぐできそうな、または実際やってみてしっくりきた部分をいくつか抜粋して備忘録代わりに書いていきます。
読んだ背景
最近担当サービスのリファクタリング業務を行っており、並行して単体テストを書く必要があったため手に取りました。
また、Scalaの関数型言語としての特性や業務で使っているクリーンアーキテクチャを活かした単体テストを書く上でも本書が役に立つといった話を伺ったので読むことにしました。
得られたこと
単体テストの定義について
本書では以下の要素を持つものを単体テストと定義しています。
- 1単位の振る舞いを検証する
- 実行時間が短い
- 他のテストケースから隔離された状態で実行される
隔離の考え方には大きく二つの派閥が存在し、それぞれ古典派、ロンドン学派と呼ばれます。
それぞれの主な特徴は以下です。
隔離対象 | 単体の意味 | テストダブルの置き換え対象 | |
---|---|---|---|
ロンドン学派 | 単体 | 1つのクラス | 不変依存以外の全ての依存 |
古典派 | テスト・ケース | 1つの振る舞い | 共有依存 |
ちなみに本書は圧倒的古典派推しで、この後も古典派の良さを説いていきます。
読みやすいテストを書くために
読みやすい単体テストにするため、以下を守るよう勧められています。
- AAAパターンで書く
Arrange(準備)
Act(実行)
Assert(確認) - 実行部分のコードは1行
- 超えるならAPIの設計そのものに問題がある
- テスト対象は SUT(System Under Test) と書く
- テスト対象をわかりやすくするため
- テスト名に厳格なルールを作らない
- 非開発者でも分かる言葉で
- テスト名にメソッド名を入れない
- メソッドの詳細ではなく、あくまで振る舞いに対するテストを行うから
- できるだけパラメータ化テストを用いる
- 壊れにくい
細かい説明は省きますが、上記を意識することで読みやすいテストが書けると説いています。
実際書いてみるとこんな感じになります。 by ChatGPT
import org.scalatest.funsuite.AnyFunSuite
// テスト対象のクラス
class Calculator {
def add(a: Int, b: Int): Int = a + b
}
class CalculatorTest extends AnyFunSuite {
test("足し算の結果を返す") {
// Arrange
val sut = new Calculator()
val a = 3
val b = 5
// Act
val result = sut.add(a, b)
// Assert
assert(result == 8)
}
}
AAAパターンでテストを書くとArrange(準備)がテストケース内に入り込むので、他テストと共有できずDRYに書けないのが欠点にも思えますが、読みやすさ重視でこのパターンに沿って書くべきだと感じました。
良い単体テストを書くためには
本書では良い単体テストは4本の柱を備えており、このうちどれか1つでも欠けていたらそのテストケースは価値がないものとみなされると説いています。
4本の柱
- 退行に対する保護
- バグへの耐性
- リファクタリングへの耐性
- テストが失敗することなくどれだけ実行できるか
- 迅速なフィードバック
- 保守のしやすさ
これらを全て最大限有することはできません、なぜなら上記のうち最初の3本はそれぞれ排反する性質を持つからです。
加えて、リファクタリングへの耐性は0か100かしかないため削ることはできず、これが最も重要な柱になります。そのため単体テストを作る際はリファクタリングへの耐性を最大限持ち、単体テストで実現したいことを見極めつつ、残りの2本を程度によって調整する必要があります。
とわいえ、私は単体テストを書き始めたばかりなのでその辺りの調整はまだまだ経験的にも知識的にも難しいです。なのでまずはリファクタリングへの耐性を最大限に保つ努力からするよう心がけています。
そのためにすべきなのが最終的な結果の確認に重きを置くことだと本書は解いています。
先ほどのパラメータテストや、ブラックボックステストにも通じる部分がありますが、とにかく実装の詳細ではなく、最終的な結果の確認(振る舞い)に重きをおいて単体テストを書くようにしています。
わかりやすい言葉で以下のように紹介されています。
振る舞いの場合
私が犬の名前を呼ぶと、その犬は私のところに寄ってきます。
詳細の場合
私が犬の名前を呼ぶと、その犬は左前足を動かし、次に右前足を動かし、頭を私の方に向け、しっぽを振り始め・・・
テストダブルの使い方
単体テストではたびたびダブルを使うことになりますが、そもそもダブルにもいくつか種類があります。
よく言われるのが、mockとスタブです。
(mockだけでダブルと同じ意味で使うことも結構ある気がします。)
mock
外部とのコミュニケーションで副作用がある場合に使う
DB操作とか、外部APIとか
stub
内部のコミュニケーションに使う
何かのusecaseからservice参照したりとか
前述の通り古典派は実装の詳細に関心を示さないので、必然的にmockをなるべく使わずテストを書こうとします。
mockを使うのは、アプリケーションを超えるコミュニケーションで、副作用がある時だけです。(外部APIとか、DBとか)
それ以外の基本的にアプリケーション内で参照しているものはスタブを使うべきと説いています。
また、スタブはあくまでテスト対象の振る舞いに必要な部品の一部という位置付けなので、検証はしません。
実際書いていて感じたこと
関数型との相性の良さ
関数型言語は純粋関数を使うのを基本としています。
純粋関数とは副作用を持たない関数です。なので、スタブが非常に使いやすく無闇にmockを使わなくて済むだけでもかなり壊れにくいテストを作れる気がします。
また、関数型ではシグネチャ(メソッドの引数、返り値の型)を厳密に書くので、パラメータテストするのにシグネチャだけ見れば何をしたいか分かるので非常に便利です。
クリーンアーキテクチャとの相性の良さ
現在担当しているサービスでは、クリーンアーキテクチャに準拠してコードを書いています。クリーンアーキテクチャは依存の方向がしっかり決まっているため、ドメイン領域でmockを多用せずに済むのが非常にありがたいです。
また、副作用を起こす領域がはっきりしているのと、依存性逆転の法則で依存する時には基本的に抽象に依存しているおかげで、スタブが作りやすいです。
全体通して
本書の中ではしきりに「実装の詳細をテストするな、観察可能な振る舞いをテストしろ」と説いていました。
それを表す言葉として以下が刺さりました。
抽象化する対象をテストするより、抽象化された結果をテストする方が簡単である。
せっかく関数型+クリーンアーキテクチャで書いているので、存分に強み活かしてテスト書いていきたいですね。
まとめ
本書内では他にも関数型へのリファクタリングや、統合テスト、DBのテストについて書かれていますが意識するだけで効果のある単体テストが書けそうな要素を今回はピックアップしました。
間違っている点や至らない点あると思いますが、何かありましたらコメントで書いていただけますと幸いです。
書籍の紹介
単体テストの考え方、使い方
その他参考書籍
なっとく!関数型プログラミング
終わりに
明日は、@wc_yamasaki さんの投稿になります。
ウェブクルーでは一緒に働いていただける方を随時募集しております。
お気軽にエントリーくださいませ。
https://www.webcrew.co.jp/recruit/