トートロジー試験







こんにちは 私の名前はArtyomであり、勤務時間のほとんどはSeleniumとCucumber / Calabashで複雑なセルフテストを書いています。 正直なところ、私は難しい選択に直面します:機能の特定の実装をテストするテストを書くこと(簡単だから)または機能をテストするテスト(より正確だがはるかに複雑だから)? 最近、実装テストが「トートロジー」テストであるという良い記事に出会いました。 そして、それを読んで、私はもう1週間近く、別の方法でいくつかのテストを書き直しています。 彼女があなたにも考えてほしいと願っています。







誰もが、高品質のソフトウェアを迅速に作成するにはテストが不可欠であることを知っています。 しかし、私たちの生活の他のすべてと同様に、不適切に使用されると、彼らは善よりも害を及ぼす可能性があります。 次の簡単な機能とテストを検討してください。 この場合、作成者は外部の依存関係からテストを保護するため、スタブが使用されます。







import hashlib from typing import List from unittest.mock import patch def get_key(key: str, values: List[str]) -> str: md5_hash = hashlib.md5(key) for value in values: md5_hash.update(value) return f'{key}:{md5_hash.hexdigest()}' @patch('hashlib.md5') def test_hash_values(mock_md5): mock_md5.return_value.hexdigest.return_value = 'world' assert get_key('hello', ['world']) == 'hello:world' mock_md5.assert_called_once_with('hello') mock_md5.return_value.update.assert_called_once_with('world') mock_md5.return_value.hexdigest.assert_called()
      
      





よさそう! コードが期待どおりに機能することを確認するために、4つのステートメントが完全にテストされます。 テストも合格です!







 $ python3.6 -m pytest test_simple.py ========= test session starts ========= itemstest_simple.py . ======= 1 passed in 0.03 seconds ======
      
      





もちろん、問題はコードが間違っていることです。 md5は、 str



ではなくbytes



のみを受け入れstr



この投稿では、Python 3でbytes



str



がどのように変化したかを説明しbytes



)。 テストシナリオは大きな役割を果たしません。 ここでは文字列の書式設定のみがテストされており、誤ったセキュリティの感覚を与えています。コードが正しく記述されているように思われ、テストスクリプトの助けを借りてそれを証明しました。







幸いなことに、 mypyはこれらの問題をキャッチします。







 $ mypy test_simple.py test_simple.py:6: error: Argument 1 to “md5” has incompatible type “str”; expected “Union[bytes, bytearray, memoryview]” test_simple.py:8: error: Argument 1 toupdateof “_Hash” has incompatible type “str”; expected “Union[bytes, bytearray, memoryview]”
      
      





最初に、文字列をバイトにエンコードするようにコードを修正しました。







 def get_key(key: str, values: List[str]) -> str: md5_hash = hashlib.md5(key.encode()) for value in values: md5_hash.update(value.encode()) return f'{key}:{md5_hash.hexdigest()}'
      
      





これでコードは機能しますが、問題は残ります。 誰かが私たちのコードを調べて、数行に単純化したとしましょう:







 def get_key(key: str, values: List[str]) -> str: hash_value = hashlib.md5(f"{key}{''.join(values)}".encode()).hexdigest() return f'{key}:{hash_value}'
      
      





機能的にはソースコードと同じものを入手しました。 同じ入力に対して、常に同じ結果を返します。 ただし、この場合でも、テストは失敗します。







 E AssertionError: Expected call: md5(b'hello') E Actual call: md5(b'helloworld')
      
      





この単純なテストには明らかに問題があります。 ここで、同時に、第1種のエラー (コードが正しくてもテストはクラッシュします)と第2種のエラー (コードが間違っていてもテストはクラッシュしません)があります。 理想的な世界では、コードにエラーが含まれている場合(およびその場合のみ)、テストは失敗します。 さらに理想的な世界では、テストに合格すると、コードの正確性を完全に確信できます。 そして、両方の理想は達成不可能ですが、彼らのために努力する価値があります。







上記のテストは、私が「トートロジー」と呼んでいるものです。 彼らはコードの正確さを確認し、書かれたとおりに実行されることを保証します。もちろん、正しく書かれていると仮定します。













トートロジーのテストは、コードにとって明確なマイナスだと思います。 いくつかの理由により:







  1. トートロジーのテストにより、エンジニアは自分のコードが正しいという誤った印象を与えます。 彼らは高いコードカバレッジを見て、プロジェクトを楽しむことができます。 同じコードベースを使用している他の人々は、テストが実際に何もテストしていないにも関わらず、テストがパスする間、自信を持って変更をプッシュします。
  2. トートロジーテストは、コードが意図したとおりに動作することを確認するのではなく、実際に実装を「フリーズ」します。 実装の側面を変更する場合は、テストを変更し、予想される出力が変更されたときにテストを変更しないことで、これを反映する必要があります。 これにより、エンジニアは実行中にエラーが発生した場合にテストを調整し、テストが失敗する理由を見つけないようになります。 これが発生すると、テストに負担がかかり、バグが本番環境に入るのを防ぐためのツールとしての本来の意味が失われます。
  3. 静的分析ツールは、とにかくトートロジーのテストで検出された可能性のあるタイプミスなど、コード内の重大なエラーを検出できます。 静的分析ツールは、特に動的言語において、過去5年間で大幅に改善されました。 たとえば、PythonのMypy 、PHPのHack 、JavaScriptのTypeScriptなどです。 それらはすべて、コードをより理解しやすくナビゲートしやすくするので、多くの場合、タイプミスをキャッチするのにより適していますが、エンジニアにとってはより価値があります。


言い換えれば、トートロジーテストはしばしば実際の問題を見逃し、盲目的にテストを修正するという悪い習慣を刺激し、それらの利点はそれらをサポートする努力を返済しません。







テストを書き直して出力を確認しましょう。







 def test_hash_values(mock_md5): expected_value = 'hello:fc5e038d38a57032085441e7fe7010b0' assert get_key('hello', ['world']) == expected_value
      
      





get_key



の詳細はテストにとって重要ではありませんget_key



が間違った値を返す場合にのみ失敗します。 オプションで、テストを更新せずにget_key



の内部を変更できます(公開動作を変更するまで)。 この場合、テストは短く、理解しやすいです。







これは不自然な例ですが、コードの範囲を広げるために、外部サービスの出力が実装の期待を満たしていると想定される実際のコードの場所を見つけるのは簡単です。







トートロジー検査を特定する方法



  1. 更新時に失敗するテストは、テストされたコードである可能性がはるかに高くなります。 コードカバレッジの価格を支払うたびに。 この価格がテストから得られる利益を上回る場合、テストは実装に密接に関連している可能性があります。 関連する問題:テストされたコードの小さな変更には、さらに多くのテストの更新が必要です。
  2. テストコードは、実装との調整なしでは編集できません。 この場合、トートロジー検査を受けた可能性が高くなります。 「トイレでのテスト:モックを使いすぎないでください」には、非常に馴染みのある例があります。 このテストに基づいて、実装自体を再作成できます。







     public void testCreditCardIsCharged() { paymentProcessor = new PaymentProcessor(mockCreditCardServer); when(mockCreditCardServer.isServerAvailable()).thenReturn(true); when(mockCreditCardServer.beginTransaction()).thenReturn(mockTransactionManager); when(mockTransactionManager.getTransaction()).thenReturn(transaction); when(mockCreditCardServer.pay(transaction, creditCard, 500)).thenReturn(mockPayment); when(mockPayment.isOverMaxBalance()).thenReturn(false); paymentProcessor.processPayment(creditCard, Money.dollars(500)); verify(mockCreditCardServer).pay(transaction, creditCard, 500); }
          
          







トートロジー検査の修正方法



  1. I / Oをロジックから分離します。 エンジニアがほとんどの場合スタブを使用するのは、I / Oのためです。 はい、入力/出力は非常に重要です。これがないと、プロセッササイクルをスクロールして空気を温めるだけです。 ただし、入力/出力をロジックと混合するよりも、コードの周辺に転送する方が適切です。 Python Sans-I / Oワーキンググループは、この主題に関する優れたドキュメントを開発しました。CoreyBenfieldは、PyCon 2016でのプロトコルライブラリ構築の正しい方法のプレゼンテーションでそれについてよく語りました。
  2. メモリ内のオブジェクトのスタブを避けます。 完全にメモリ内にある依存関係をスタブとして使用するには、非常に適切な理由が必要です。 基礎となる関数が非決定的であるか、時間がかかりすぎる可能性があります。 実際のオブジェクトを使用すると、テストシナリオ内でより多くの相互作用がチェックされるため、テストの価値が高まります。 ただし、この場合でも、コードがこれらの依存関係を正しく使用していることを確認するテストが必要です(出力が期待される範囲内にあることを確認するテストなど)。 以下は、 randint



    が特定の値を返す場合にコードが機能すること、およびrandint



    呼び出すことを確認する例です。







     import random from unittest.mock import patch def get_thing(): return random.randint(0, 10) @patch('random.randint') def test_random_mock(mock_randint): mock_randint.return_value = 3 assert get_thing() == 3 def test_random_real(): assert 0 <= get_thing() < 10
          
          





  3. 補助データを使用します。 スタブ依存関係が外部サービスとして使用される場合、偽データのセットを作成するか、スタブサーバーを使用して補助データを提供します。 偽の実装を一元化することで、実際の実装の動作を注意深くエミュレートし、実装変更中のテスト変更の量を最小限に抑えることができます。
  4. コードの一部を明らかにすることを恐れないでください! 適切なコードテストとテストなしのどちらかを選択した場合、答えは明らかです。 しかし、トートロジーのテストとテストの欠如のどちらかを選択する場合、すべてがそれほど明白ではありません。 トートロジーのテストは悪であると私があなたに確信したことを願っています。 コードの一部を露出したままにすると、他の開発者にとって現在の状況を示す一種の指標になります。コードのこの部分を変更するときには、注意を払うことができます。 または、より好ましくは、上記の手法を使用して適切なテストを記述します。








十分にテストされているという錯覚を作成するよりも、コードの行を露出したままにしておくことをお勧めします。







他の人のコードを修正するときは、トートロジーテストにも注意を払ってください。 このテストがコード行をカバーするかどうかだけでなく、実際にチェックするものを自問してください。







トートロジー検査は良くないので悪いことを覚えておいてください。







トピックに関する読み物






All Articles