可読テスト

エントリー

この記事は、非常に読みにくいテストを書くためのアンチパターンを使用してコードを表示するという繰り返しの会議の結果として書かれました。 この記事のフレームワークでは、読み取り可能なテストの理論を明らかにし、思慮深い命名と補助的な方法の有能な適用によって識別された特性を達成する方法を示します。



単位 これは何ですか

単体テストは通常​​、単体テストとしてロシア語に翻訳されます。 ただし、「モジュール」という単語には、展開スキームに関連付けられたわずかに異なる意味的意味があります。 したがって、不要な関連付けを避けるために、英語の「ユニット」を使用します。 ユニットテスト用語の枠組みの中でユニットが何であるかをもう一度思い出してみましょう。



ユニットは、特定の環境で特定の入力データを使用して特定の出力を提供するコードです。


ユニット定義

ユニット自体に加えて、この定義の他のすべてのコンポーネントを空のセットに縮退することができますが、この混乱の空の参加者が多いほど、ユニットに含まれる感覚(セマンティクス)が少なくなります。





モロクに犠牲にならないように、ニーチェの言葉による抽象化では、この愛の広場の各構成要素を明確に示すいくつかの例を考えてみましょう。



空の環境
これは、構造分解のスタイルで記述されたユニットの最も一般的な構成です。 引数を取り、結果を返す関数は、 入力として多くの引数を持ち、 出力として結果を持つユニットの例です。 結果と入力パラメーターとの関係は、関数の本質です。 たとえば、関数y = sin(x)は、すべての実数のセットの区間[-1,1]へのマッピングです。 各入力xに対して、yは一意に定義され、sin(x)の責任はこの一意性を保証することです。 sin(x)は、これらのxをこれらのyに、すべてのユーザーのあらゆる環境で、いつでもマッピングします。 彼は環境に興味がありません。



空の出力
多くの場合、ユニットの責任は何かを返すだけでなく、何らかの状態で何かをすることです。 たとえば、古典的なメディエーターパターンは、特定のイベント( 入力データ )を受け取り、オブジェクトのセット( 環境 )でいくつかのメソッドを呼び出すオブジェクト( ユニット )の存在を想定しています。 このオブジェクトは誰にも何も返しません。その責任は、バインドするために呼び出されるオブジェクトのセット間に不変式を提供することです。 これらの不変条件の提供は彼の責任です。 これは、環境に対する入力イベントの影響です。これは、ユニットメディエーターの定義です。 ラジオボタンをクリックすると、ダイアログの特定のセクションをアクティブにするメディエーターウィンドウは、この構成のユニットの具体例です。



空の入力
このようなユニットの存在の必要性は、どこかでデータが発生し、外部から供給されるべきではないという単純な論理的結論に基づいています。 そのようなユニットの良い例はGetCurrentTime()( unit )関数です。これはシステム( environment )から現在の時間を減算し、クライアントにそれを返します( output data )。



ボイドf()
何も受け入れず、何も返さず、環境と相互作用しない関数はほとんど意味がありません。 これ自体は特定のことであり、面倒なことはしないでください。使用も作成もしないでください。



ステータスとユニット
上記の例と定義に照らして、関数void std :: list :: remove(const T&elem)を見てみましょう。 elemが入力されたとしましょう。 しかし、環境と出力はどうでしょうか? 出力はありません-void関数。 そして、環境は何ですか? 正式に-メモリ。 ただし、メモリはリストよりも下位レベルのエンティティです。 結局のところ、だれも「リストから削除することはトリッキーなスキームに従ってメモリを解放する」と主張することはないでしょう。 「リストからの削除」の意味(意味)を判断しようとすると、次のように聞こえます:「リストから要素を削除すると、リストの状態が変化するため、リストからこの要素を取得できなくなります」。 したがって、リストからの削除のセマンティクスの定義は、リストインターフェイスからの別の操作を使用して、削除自体と同じ抽象化レベルで行わます。 したがって、リスト:: remove関数自体はunitではありません 。 remove関数自体をすべてから分離して決定(および検証)することは不可能です。 リストは、それに対する操作の全体においてのみ意味があります。 リストは単位です。 それへの入力は多くのペア(command、command_arguments)であり、ペアの出力(query、query_results)です。 ちなみに、このSTL設計はCQS(コマンドクエリ分離)の原則に準拠しています。この原則では、状態を変更してそれを返すメソッドは互いに分離されます(これは、特に安全のために行われました)。 ただし、一般的なケースでは、クラスには多くの出力があり、入力を持つ一部の操作の結果も使用できます。 悪いが、可能。 ところで、問題は、環境はどうですか? std :: listも環境に依存しませんか? いや! アロケーターを送信できます! メモリの操作は、ロケーターを介したリストです。 リストは環境への依存を明確に宣言しました。 よくやった!



読み取り可能なテスト

単体テストの理論は、テストの可読性について話すと約束したときに、それとどう関係するのでしょうか? 答えは簡単です。



ユニットテストは読みやすく、ユニットの決定に関係する4つのコンポーネント( ユニット入力データ、出力データ、 環境)がすべて明らかです。


自明性といえば、テストが書かれているサブジェクト領域のコンテキストのみを持ち、テストのみを読み、別のコンテキストの詳細を明確にすることによって気を散らされないサードパーティの読者は、テストの本質とそれによって作成されたステートメントとチェックを推測できることが理解されています。 そうでない場合、テストは読みにくく、改善する必要があります-多くの場合、インターフェイスとユニットの設計を改良します。



テスト解剖学

特定の推奨事項を提示する前に、あいまいさがないようにいくつかの定義を作成します。 したがって、 gtest単体テストフレームワークを使用して記述された典型的なテストは次のようになります。

TEST(Subject, Assertion) { // Body }
      
      





または:

 TEST_F(Subject, Assertion) { // Body }
      
      





ルール



明示的なユニット
ルール:被験者は、テスト対象のユニットまたはシステムを一意に指す必要があります。

説明:対象は、ユニットを示す英語の名詞(できれば非複合語と略称)でなければなりません。 このファイル内のすべてのテストには、サブジェクトが含まれるこのユニットが必要であり、同じサブジェクトを持つTESTマクロとTEST_Fマクロの混在は禁止されています。

例:



悪い:

 TEST(GetDiskSignature, ReadsFirstSector) TEST(SetDiskSignature, WritesToFirstSector)
      
      





良い:

 TEST(DiskSignature, ResidesOnFirstSector)
      
      





テスト予測
規則:アサーションには、意味検証の対象となる英語の完全なステートメント(現在時制の物語文)を含める必要があります。

説明:ステートメントを読むことから、読者はテストの内容に期待する必要があります。 つまり、特定のサブジェクトに関するこのアサーションを読んだ後、読者は自分でこのステートメントを検証するかのように、頭に計画を立てる必要があります。 そのため、述語の述語として、「正しく」、「良い」、「細かい」、「よく」などの偽りの言葉や、同様の倫理的および道徳的な言葉は厳密に禁止されています。 テストの本質は、「正しい」、「良い」などの意味を正確に明らかにすることです。 述語内のそのような単語は、定義自体としてではなく、定義済みエンティティとして許可されます。

例:



悪い:

 TEST(FileCache, IsInitializedCorrectly)
      
      





良い:

 TEST(FileCache, IsInitializedAsEmpty)
      
      







ボディ内のアサーションからのエンティティの使用
ルール:テスト本体で使用される識別子は、ステートメントで使用される用語を再利用する必要があります。

説明:テストを読むとき、文から用語を逐語的に繰り返すよりも、読者にとってより明白なものは何でしょうか? 読者がアサーションで何らかの質の約束をし、テストで何が起こるかについて期待している場合、ステートメントから用語を繰り返すなど、使用されたエンティティにしがみつくのを助けることはできません。 ケース、番号、あらゆる種類の短い接尾辞や接頭辞の使用など、わずかな変更が許可されます-人間の脳(特にロシア語)は、元のシンボルと同等であると見なして、そのようなシンボル変換と非常に効果的に戦います。 この用語の突然変異が多いほど、読者がそれを認識するのが難しくなります。 したがって、同義語でさえ歓迎されません。

例:



悪い:

 TEST_F(MRUCache, MovesLastAccessedItemToFront) { Items.Touch("http://facebook.com/"); Items.Touch("http://habrahabr.ru/"); EXPECT_EQ(0, Items.GetIndex("http://habrahabr.ru/")); }
      
      





良い:

 TEST_F(MRUCache, SetsIndexOfLastTouchedItemToZero) { MRUCache.Touch(Item("http://facebook.com/")); MRUCache.Touch(Item("http://habrahabr.ru/")); EXPECT_EQ(0, MRUCache.GetIndex(Item("http://habrahabr.ru/"))); }
      
      





良い:

 TEST_F(MRUCache, MovesLastAccessedItemToFront) { MRUCache.Access(Item("http://facebook.com/")); MRUCache.Access(Item("http://habrahabr.ru/")); EXPECT_EQ(Item("http://habrahabr.ru/"), MRUCache.Front()); }
      
      







テストデータの明確性
ルール:すべてのテストデータ(入力と出力の両方)は、環境から読み取られたデータまたは環境に書き込まれたデータと同様に、テストの本文に存在する必要があります。

説明:読むとき、サードパーティの読者が最初にしがみつくのは-主題の専門家-おなじみのデータです。 ゆりかごの人は、特定の知識から一般的な知識まで自分の知識を構築します。したがって、その人は、残りのすべての中で特定の特異性を最もよく認識します。 テストの最高の表現力が達成されるのは、1つのコンテキストで収集されたデータの選択です。 これは読み取り可能なテストの非常に重要な側面であるため、このルールのアンチパターン違反は別の記事に掲載され、それらに対抗する代替手段が示されます。

例:



悪い:

 const Common::String SomeUnixPath = GET_WCHAR("/var/log"); const int SomeUnixPathComponents = 2; ... TEST(UnixPath, ContainsSlashes) { EXPECT_EQ(SomeUnixPathComponents, Paths::Unix(SomeUnixPath).Components()); }
      
      





良い:

 Common::String Path(const char* value) { return GET_WCHAR(value); } ... TEST(UnixPath, ContainsSlashes) { EXPECT_EQ(2, Paths::Unix(Path("/var/log")).Components()); }
      
      







テストデータフローの明確さ
規則:テストの本体には、ユニットを通るデータストリームに参加するすべての補助オブジェクトが含まれている必要があります。

説明:単体テストを作成するとき、表現力のある名前を持つ補助関数で頻繁に繰り返される操作が通常行われます(これは良いことです)が、TEST_Fマクロを使用すると、明示的に渡さずにこれらの関数でフィクスチャメンバーを使用する致命的な傾向があります。 その結果、これらのメンバーは最終的にテスト対象のユニットに移動しますが、補助機能に移動してコードを読み取ることで、テストが何らかの形でその内容に影響を与えたことがわかります。 つまり、理解のためのテストには、異なるコンテキストへの移行が必要であり、読みやすさが低下します。 したがって、補助機能が他の方法でテストデータにアクセスできる場合でも、テスト本体から明示的に渡して、データフローとユニットの動作への影響を示す必要があります。

例:



悪い:

 MockFileSystem Files; void AddFile(std::string path, int size) { ON_CALL(Files, Get(path.c_str()).WillByDefault(Return(File(size))); } ... TEST(FileStatistics, SumsFileSizes) { AddFile("/bin/ls", 10); AddFile("/bin/bash", 20); EXPECT_EQ(30, GetStats(Files, "/bin").Size); }
      
      





良い:

 MockFileSystem Files; void AddFile(MockFileSystem& fs, std::string path, int size) { ON_CALL(fs, Get(path.c_str()).WillByDefault(Return(File(size))); } ... TEST(FileStatistics, SumsFileSizes) { AddFile(Files, "/bin/ls", 10); AddFile(Files, "/bin/bash", 20); EXPECT_EQ(10 + 20, GetStats(Files, "/bin").Size); }
      
      







おわりに

この記事では、読み取り可能なテストを作成する理論と実践について説明します。 与えられた例は、テストを改善するトリックの全範囲を使い尽くすものではありません-これらは、他の人が見たテストとまったく同じように、私がテストで気づいた単なる圧迫です。 基本的なルールは、自給自足のテストを書くことです。 補助関数に何かが入った場合、そのシグネチャと同様に、関数自体の名前は非常に明白である必要があるため、その実装を監視する必要はありません。 必須ではないものを隠して、必須のものに名前を付けてください!



All Articles