テストを自動化したいが,テストケースが多すぎると開発速度が落ちる.
(全体の自動テストの実行時間が遅くなる.確認・修正のサイクルも遅くなる)
品質を自動で担保したい時に考えていることをまとめてみた.
コードの正しさを自動化で確認する方法
現実的な方法としては,大きく分けて二つある.
- 静的解析(契約で保証する)
- 自動テスト(実行で保証する)
個人的には,静的解析に頼ることが多い.
静的解析できないことを自動テストで補うスタイルをとりがち.
静的解析
ファイル保存などのタイミングで型の整合性を確認し,定義に合わないコードを自動で検出させる.
インターフェースによる契約で品質を担保する技術とも言える.
Rust とか関数型言語の流れを強く受けた言語でよく使う.
完全に静的解析だけで保証する方針の場合,Rustでは cargo check
をCIにさせればよい.
メリット
- 異常に気付くタイミングが早く,対応を打ちやすい
- テストコードを書く必要がなく,定義変更に伴う影響を網羅的に保証できる
デメリット
- 型で表現できない問題は対象外(実行時にパニックが起こるものなど)
- 味方のはずのコンパイラと戦うことになり,開発速度が低下することがある
自動テスト
テストコードを書き,テストコードを別途実行させる.
動くコードが正義という考え方.
自動テストを行う方針の場合,Rustでは cargo test
をCIにさせればよい.
メリット
- テストコード自体が,使用方法の簡単な説明になる
- 静的解析では検知できない問題に対応できる(特に結合テストなど)
デメリット
- 倍近いコードを書くに等しい
- 複雑な対象はテストコードの組合せ爆発に繋がる
- 規模が大きくなるとテストの実行時間も長くなる(負債扱いされだす)
静的解析する際に意識してきたもの
エディタの PROBLEMS を常に0にする
Warning や Error がひとつも出ない状態を維持する.
自分の加えた変更による影響点が原因なのか,元々問題であったのかを切り分けるため.
早い段階で型を断定する
曖昧なコードは抽象的とは違う.
入力は受け取った段階で具体的な型に変換し,静的解析の恩恵を最大限に利用する.
(具体的には JSON などのをシステムの内部で使い回さず,早い段階で構造体にマップする)
具体的な型を使う
NewType パターンはうっかり間違った情報源からデータを取ってこないように,よく使う.
コンテキストにあった型を使った方が,間違えにくく,コードも追いやすくなる.
// 間違えやすい
fn get_prize(id: uuid::Uuid) -> Result<Prize, Error> {
...
}
// 間違えにくい
struct PrizeUuid(uuid::Uuid);
fn get_prize(id: PrizeUuid): -> Result<Prize, Error> {
...
}
自動テストで意識してきたもの
宣言的なものはテストしない
例えば,宣言的なバリデーションを利用しているときは,それをテストをしない.
class UserValidator(Validator):
@not_null
@min(5)
@max(50)
name: int
...
宣言的なテストは,いつも奇妙なテスト内容だ.
「Null を入れたらエラーになる時,Null を入れたらエラーになること」
この場合,テストで捕まる要因はバリデータの書き忘れだが,
バリデータを書き忘れるときは,テストケースも書き忘れる.
バリデータの定義を確認するだけの方が効率が良い.
静的解析で保証できることも,もちろんテストしない.
公開されたインターフェースをテストする
インターフェース上できない操作は起こる事がない.
(もちろん,インターフェースに基づいた設計をしていればの話だが...)
クラスのインターフェースの単位で単体テストを書いている.
下記は単体テストで書かない
- private な関数のテスト
- 単体テストされた別のクラスを内部で利用した際,その別のクラスに関するテスト
一つのクラスを多機能化させない
クラスのインターフェースが増えれば組み合わせによるテストケースも増える.
特に,内部状態が変化するインターフェースには注意が必要だ.
クラス設計は 1 つの機能を上手くやるレベルの方が,バグは少ない.
class CsvWriter:
def write(self, file: File):
...
class FileChecker:
def check(self) -> bool:
...
テストコードを他人への説明に利用する
どのような条件の時にテスト対象が使えるのかを説明するようにテストを書く.
( DocTest の考え方をテストでよく利用する)
# テスト対象が使える状況までの道筋(使用方法)
let user = create_user()?;
let campaign = create_campaign(user)?;
let prize = create_prize(campaign)?;
# テスト対象
let response = api::update_prize(prize)?;
# テスト判定
assert_eq!(response.status, RESPONSE_SUCCESS);