テストを書くためのベストプラクティス
この章では、テストの作成と変更に関するベストプラクティスについて説明します。 私たちが作成するテストは、何年も後になっても、元の作成者に確認したり、 大量の git 考古学を行ったりする必要なく、理解しやすく変更しやすいものにしたいと考えています。
自分が作成したテストを、数年後にあまり文脈がない状態で失敗したテストを見ている 別のコントリビューターになったつもりでレビューするのはよい習慣です (これは数日後や数か月後の自分自身にも役立ちます!)。 そして自問してください。どうすれば自分やその人たちの作業を楽にできるだろうか?
これを具体的に捉えやすくするため、まずは、別のコントリビューターの作業を 可能な限り困難にするテストを書く方法についての余談から始めましょう。
余談: シンプルなテスト妨害フィールドマニュアル
別のコントリビューターの作業を可能な限り困難にするには、次のようなことをするとよいでしょう。
- テストに、他の文脈を一切含めず issue 番号だけの名前を付ける。例:
issue-123456.rs。- そのテストが何を検証しようとしているのかについてのコメントや、関連する文脈へのリンクを 一切含めない。
- (本来は最小化できるにもかかわらず)巨大なテストを含め、 そのテストが実際にテストしようとしている核心から注意をそらす、 本質的でない部分を含める。
- テストが確認しようとしている内容にとって重要ではない、無関係な構文エラーやその他のエラーを 大量に含める。
- スニペットを奇妙な形式で整形する。
- 使用されておらず無関係な機能を大量に含める。
- たとえば
ignore-windowscompiletest directives を含めるが、それらが なぜ 必要なのかについて説明しない。
テストの命名
読者が、issue 番号を入力して github 検索を掘り進め、
そのテストが何を検証しようとしているのかを探さなくても済むように、
テストが何を検証しているのかをすぐに理解できるようにします。
これには、関連するテストの集まりとして --test-args でテストをフィルターできるようになる、
という追加の利点もあります。
- テストが検証しようとしている内容、または回帰を防ごうとしている内容に基づいて名前を付ける。
- 簡潔に保つ。
- issue 番号だけをテスト名として使うのを避ける。
- 自動補完の劣化につながるため、テスト名を
issue-xxxxxプレフィックスで始めるのを避ける。
issue 番号だけをテスト名として使うのを避ける
代わりに、テストコメント内のリンクや
#123456として含めることを推奨します。あるいは、 issue 番号を含めることに意味がある場合は、macro-external-span-ice-123956.rsのような短いキーワードも含めてください。tests/ui/typeck/issue-123456.rs // 悪い tests/ui/typeck/issue-123456-asm-macro-external-span-ice.rs // 悪い(タブ補完にとって) tests/ui/typeck/asm-macro-external-span-ice-123456.rs // 良い tests/ui/typeck/asm-macro-external-span-ice.rs // 良い
issue-123456.rsは、そのテストが実際に何を検証しているのかをすぐには何も教えてくれないため、 追加で検索する必要があります。テスト名のプレフィックスとして issue 番号を含めると、 タブ補完の有用性が下がります (テストディレクトリでlsしたときにissue-xxxxxプレフィックスが大量に出てくる場合など)。 issue へのリンクはテストコメント内に置くことができます。//! 外部クレート由来のネストしたマクロを含む `asm!` マクロが、 //! コードポイント境界アサーション ICE につながらないことを確認する。 //! //! <https://github.com/rust-lang/rust/issues/123456> の回帰テスト。このルールの例外の 1 つが crash tests です。そこでは、 テストが issue 番号だけで命名されるのが標準です。というのも、その目的は、 もはや ICE/クラッシュしなくなった issue 由来のスニペットを追跡することであり、 それらは修正 PR で削除されるか、適切な ui/その他のテストへ変換されるためです。
テストの構成
- ほとんどのテストスイートでは、テストを置くための意味的に適切なサブディレクトリを見つけるようにしてください。
- たとえば、RFC 2093 の実装に特化した場合、
tests/ui/rfc-2093-infer-outlives/配下に一連のテストをまとめることができます。 ディレクトリ名には、その RFC が何に関するものかを含めてください。
- たとえば、RFC 2093 の実装に特化した場合、
run-make/run-make-supportテストスイートでは、それぞれのrmake.rsは、 それぞれtests/run-make/またはtests/run-make-cargo/の直下のサブディレクトリ内に 含まれていなければなりません。 現時点では、さらに深いネストはサポートされていません。 この場合も、テスト名に issue 番号「だけ」を使うのは避けてください。
テストの説明
変更によってテストが失敗した場合に、他のコントリビューターがそのテストの内容を理解できるように、 テストには、その意図/目的、関連する文脈へのリンク(issue 番号やその他の議論を含む)、 場合によっては関連リソース(たとえば、特定の挙動について Win32 API へリンクすると役立つことがあります) について十分なドキュメントを含めるべきです。
よいコメントを含むテストの概要
//! テストが何を検証しているのかの簡潔な概要。
//! 例: #123456 の回帰テスト: coverage 属性が非 item に適用されたときに
//! ICE しないことを確認する。
//!
//! 任意: 関連するテスト/issue、外部 API/ツール、クラッシュの
//! 仕組み、どのように修正されたか、FIXME、制限事項などについての備考。
//! 例: このテストは `tests/attrs/linkage.rs` に似ているが、このテストは
//! 異なるコードパスを検証する `#[coverage]` を特に確認している。
//! ICE は属性検証中に、`def_path_str` を構築しようとしたものの、
//! プラットフォームが windows の場合にのみ診断を出力したため、
//! unix で ICE が発生したことで引き起こされた。
//!
//! 関連する issue や議論へのリンク。以下に例を示す:
//! <https://github.com/rust-lang/rust/issues/123456> の回帰テスト。
//! <https://github.com/rust-lang/rust/issues/101345> も参照。
//! <https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/123456-example-topic> の議論を参照。
//! [`clone(2)`] を参照。
//!
//! [`clone(2)`]: https://man7.org/linux/man-pages/man2/clone.2.html
//@ ignore-windows
// 理由: (なぜこのテストは windows で無視されるのか?なぜ特に
// windows-gnu や windows-msvc ではないのか?)
// 任意: テストケースの概要: どの肯定的ケースが確認されるのか?
// どの否定的ケースが確認されるのか?特有の癖はあるか?
fn main() {
#[coverage]
//~^ ERROR coverage attribute can only be applied to function items.
let _ = {
// 読者が注意を払うべき点を強調するコメント。
fn foo() {}
};
}
どれだけの文脈/説明が必要かは、作成者とレビュー担当者の裁量に委ねられます。 よい経験則として、テストで検証している自明でない事柄には、 他のコントリビューターが理解する助けとなる説明を付けるべきです。 これには、次のような備考が含まれる場合があります。
- ICE が発生する仕組みがかなり複雑な場合、その仕組み。
- 関連する issue やテスト(たとえば、このテストは別のテストに似ているが、 …という理由で分けて維持されている、など)。
- プラットフォーム固有の挙動。
- 外部依存関係や API の挙動: syscall、リンカー、ツール、 環境など。
テスト内容
- テストは可能な限り最小限になるようにしてください。
- 重要でないコードを最小限に抑え、特に stderr スナップショットを乱雑にし得る不要な構文エラーや型エラーを 最小限に抑えてください。
- 関係のない警告を抑制するには、
#![allow(...)]または#![expect(...)]を使用してください。 - 可能であれば、意味的に有意義な名前を使用してください(例:
fn bare_coverage_attributes() {})。
不安定なテスト
すべてのテストは、再現可能で信頼できるものになるよう努める必要があります。 不安定なテストは最悪の種類のテストであり、そもそもテストが存在しないことよりも悪いとさえ言えるでしょう。
- 不安定なテストは、まったく関係のない PR で失敗する可能性があり、他の コントリビューターを混乱させ、テストの失敗が関係しているかどうかを調べる時間を浪費させる可能性があります。
- 不安定なテストは、そのテスト結果から有用な情報を何も提供しません。 不安定で信頼できないということ以外は分かりません。テストが成功しても不安定なら、単に運が良かっただけでしょうか? テストが不安定で失敗したなら、それは単に偶発的なものでしょうか?
- 不安定なテストは、テストスイート全体への信頼を低下させます。 テストスイートが不安定なテストによって ランダムに偶発的に失敗する可能性がある場合、テストスイート全体が成功したのでしょうか、それとも 単に運が良かった/悪かっただけなのでしょうか?
- 不安定なテストはフル CI でランダムに失敗する可能性があり、それまでのフル CI リソースを浪費します。
Compiletest ディレクティブ
ディレクティブの一覧については、compiletest ディレクティブを参照してください。
ignore-*/needs-*/only-*ディレクティブについては、極めて明白な場合を除き、 そのディレクティブが必要な理由について簡潔な注釈を記載してください。例:"//@ ignore-wasi (wasi codegens the main symbol differently)"。//@ ignore-auxiliaryを使用する場合は、対応するメインのテストファイルを指定してください。 例://@ ignore-auxiliary (used by `./foo.rs`)。
FileCheck のベストプラクティス
詳細については、LLVM FileCheck ガイドを参照してください。
- 特定のレジスタ番号や基本ブロック番号がテストにとって特別または重要でない限り、 それらにマッチさせることは避けてください。 適切な場合は、それらにマッチさせるためにパターンを使用することを検討してください。
TODO
具体的な助言は未定です。