組織のガイドラインをまとめるのに役立つユニットテストのベストプラクティスを調査する際に、テストフィクスチャ(テストクラス)を分離する方が良いのか、それとも1つのファイルのすべてのテストを1つのファイルに保存するのが良いのかという疑問に遭遇しました。
Fwiw、私は純粋な意味で「単体テスト」を参照しています。これらは、単一のクラス、テストごとに1つのアサーション、モックされたすべての依存関係などを対象とするホワイトボックステストです。
シナリオの例は、CheckInとCheckOutの2つのメソッドを持つクラス(Documentと呼びます)です。各メソッドは、その動作を制御するさまざまなルールなどを実装します。テストごとに1つのアサーションルールに従って、メソッドごとに複数のテストを行います。すべてのテストを、DocumentTests
やCheckInShouldThrowExceptionWhenUserIsUnauthorized
のような名前の単一のCheckOutShouldThrowExceptionWhenUserIsUnauthorized
クラスに配置することもできます。
または、CheckInShould
とCheckOutShould
の2つの個別のテストクラスを使用することもできます。この場合、私のテスト名は短縮されますが、特定の動作(メソッド)のすべてのテストが一緒になるように整理されます。
私はどちらのアプローチにも賛否両論があると確信しており、誰かが複数のファイルを使用してルートを進んだかどうか疑問に思っています。もしそうなら、なぜですか?または、単一ファイルのアプローチを選択した場合、なぜそれが優れていると感じますか?
私が採用することを選択した手法が最初に実証されたリンクを覚えている/見つけられるといいのですが。基本的に、テストする各メンバーのネストされたテストフィクスチャ(クラス)を含む、テスト中の各クラスに対して単一の抽象クラスを作成します。これにより、本来必要な分離が提供されますが、すべてのテストが場所ごとに同じファイルに保持されます。さらに、これにより、テストランナーで簡単にグループ化およびソートできるクラス名が生成されます。
このアプローチを私の元のシナリオに適用する方法の例を次に示します。
public abstract class DocumentTests : TestBase
{
[TestClass()]
public sealed class CheckInShould : DocumentTests
{
[TestMethod()]
public void ThrowExceptionWhenUserIsNotAuthorized()
{
}
}
[TestClass()]
public sealed class CheckOutShould : DocumentTests
{
[TestMethod()]
public void ThrowExceptionWhenUserIsNotAuthorized()
{
}
}
}
これにより、次のテストがテストリストに表示されます。
DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized
テストが追加されると、それらは簡単にグループ化され、クラス名でソートされます。これにより、テスト中のクラスのすべてのテストが一緒にリストされます。
私が活用することを学んだこのアプローチの別の利点は、この構造のオブジェクト指向の性質であり、すべてのネストされたクラスとそれぞれの内部で共有されるDocumentTests基本クラスの起動メソッドとクリーンアップメソッドの内部でコードを定義できることを意味します含まれるテストのネストされたクラス。
まれですが、テスト対象の特定のクラスに対して複数のテストクラスを使用することは意味がある場合があります。通常、これは、異なるセットアップが必要で、テストのサブセット全体で共有される場合に行います。
単一のクラスのテストを複数のテストクラスに分割する説得力のある理由は本当にわかりません。原動力となるアイデアはクラスレベルでの一貫性を維持することであるため、テストレベルでも同様に努力する必要があります。いくつかのランダムな理由:
クラスの単体テストを複数のファイルに分割せざるを得ない場合、それはクラス自体の設計が不十分であることを示している可能性があります。私は、単一責任の原則と他のプログラミングのベストプラクティスに合理的に準拠しているクラスの単体テストを分割する方が有益なシナリオは考えられません。
さらに、ユニットテストのメソッド名を長くすることも可能ですが、気になる場合は、ユニットテストの命名規則をいつでも見直して、名前を短くすることができます。
テストを複数のクラスに分割することに反対する私の議論の1つは、チームの他の開発者(特にテストに精通していない開発者)が既存のテストを見つけるのが難しくなることです(このメソッドのテスト?どこにあるのだろう?)また、新しいテストをどこに置くか(このクラスでこのメソッドのテストを作成しますが、それらを新しいファイルまたは既存のファイルに入れるべきですか?)
異なるテストで大幅に異なるセットアップが必要な場合、TDDの実践者が「フィクスチャ」または「セットアップ」を異なるクラス/ファイルに配置し、テスト自体は配置しないことを見てきました。
少し考え直してみてください。
クラスごとにテストクラスが1つしかないのはなぜですか。クラステストまたはユニットテストを行っていますか? classesにも依存していますか?
ユニットテストは、特定のコンテキストで特定の動作をテストすることになっています。クラスにCheckIn
がすでにあるということは、そもそもそれを必要とする動作があるはずであることを意味します。
この疑似コードについてどう思いますか:
// check_in_test.file
class CheckInTest extends TestCase {
/** @test */
public function unauthorized_users_cannot_check_in() {
$this->expectException();
$document = new Document($unauthorizedUser);
$document->checkIn();
}
}
ここでは、checkIn
メソッドを直接テストしていません。代わりにbehaviourをテストしています(幸いにも;)でチェックインします)、Document
をリファクタリングして別のクラスに分割するか、別のクラスをマージする必要がある場合、テストファイルはリファクタリング時にロジックを変更することはなく、構造のみを変更するため、一貫性があります。
1つのクラスに1つのテストファイルを使用すると、必要なときにいつでもリファクタリングが難しくなるだけでなく、コード自体のドメイン/ロジックの観点からもテストの意味が少なくなります。
最初の開発時には、自分が何をしているかをかなりよく理解しており、どちらのソリューションもおそらく問題なく動作します。
変更後のテストが失敗したときに、それはより興味深いものになります。 2つの可能性があります。
CheckIn
(またはCheckOut
)を深刻に破壊しました。この場合も、単一ファイルと2ファイルのソリューションはどちらも問題ありません。CheckIn
とCheckOut
の両方(およびおそらくそれらのテスト)を、それぞれ一緒にではなく、それぞれに対して意味のある方法で変更しました。あなたはペアの一貫性を壊しました。その場合、テストを2つのファイルに分割すると、問題の理解が難しくなります。テストには、主に次の2つの目的があります。
これらの両方の見方は、テストをまとめることが役立つことを示唆していますが、害はありません。
一般に、テストはメソッドではなくクラスの動作をテストすることと考えることをお勧めします。つまり一部のテストでは、予想される動作をテストするために、クラスの両方のメソッドを呼び出す必要がある場合があります。
通常、私はプロダクションクラスごとに1つの単体テストクラスから始めますが、最終的には、その単体テストクラスを、テストする動作に基づいていくつかのテストクラスに分割する可能性があります。言い換えれば、テストクラスをCheckInShouldとCheckOutShouldに分割するのではなく、テスト中のユニットの動作によって分割しないことをお勧めします。