契約と単体テスト

免責事項:この注記は、読者がユニットテストに関する基本的な知識を持っていることを意味します。これらの行の作成者は疑う余地がなく、契約デザインに関する基本的な知識もここから補充できます



契約設計に捧げられたスピーチの1つで、私の同僚の1人が、契約と単体テストの関係について完全に合理的な質問をしました。 ユニットテストのようなクラス契約の事後条件は、顧客にクラス保証を語ります。また、ユニットテストはこの点でより強力なメカニズムであるため(契約という形の複雑な事後条件は必ずしも単純ではなく、不可能な場合もあります)、事後条件。



それでは、コントラクトと単体テストの共通部分がどこにあるのかを簡単に見てみましょう。単体テストが存在する場合、事後条件は冗長ですか?





契約


コードの問題は、それが自給自足ではないことです。 もちろん、コードを見ると、ソリューションの準最適性をすぐに確認でき、些細な愚かさやプログラミング言語のイディオムの誤った使用を見つけることができますが、主な質問に答えるのは非常に困難です。



問題は、コード自体が正しくない、または正しくないことです。正確さの概念は組み合わせてのみ適用できます。コードは仕様です。 (これについての詳細は、記事「 契約による設計。ソフトウェアの正確性」を参照してください。)



通常、この問題はコメント内の情報を複製することで解決されますが、これは非常に急速に古くなり、コード自体と矛盾し始めます。 仕様は正式なものであり、一部のWikiページに配置することもできます。 ステートメントの形式で表現でき、ユニットテストもこれらの目的に使用できます。



後者の方法については後で検討しますが、今のところは最後から2番目の方法、契約の使用に進みましょう。





契約は、ステートメントの形式でコードで仕様を直接指定するように設計されています:前提条件、事後条件、不変条件、およびステートメント。 今、これらの問題のすべての微妙さに興味があるわけではありません。今のところ、前提条件と事後条件のみを考慮するだけで十分です。



クラスBメソッドの前提条件は、クライアントコントラクトのことです。クラス B が職務を果たす ためにクラス B クライアントがしなければならないこと 。 クラスBメソッドの事後条件は、顧客の前にクラスB契約について 述べています。これは、顧客が契約の一部を履行した場合 にクラス B の履行を保証します



コントラクトの例は、 IListインターフェイスのAddメソッドのコントラクトです。IListインターフェイスのクライアントがゼロ以外のオブジェクト(前提条件)を渡すと、このコレクションに追加され、要素数(Count)が1(事後条件)増加します。



単体テスト




ユニットテストは現在非常に人気のある手法であり、リファクタリングプロセスの簡素化からモジュール性の向上や接続性の緩和によるアプリケーションの設計の改善に至るまで、開発者は多くの利点を得ることができます。



単体テストのもう1つの特性は、クラスまたはモジュールの予想される動作を本質的に記述することです。これは、仕様と見なすこともできます。 テストは、特定のクラスの作業を理解したいチームの新しいメンバーにとって素晴らしい出発点です。なぜなら、彼らの助けを借りて、クラスの使用方法、必要な入力、期待される結果がわかるからです。



クラス契約の前提条件と事後条件によく似ているのは、単体テストの最後の側面です。 実際、確かに類似点はありますが、重要な違いがあります。

契約は宣言的です。顧客へのクラス保証を高レベルで説明しますが、これらの保証がどのように提供されるかについては何も言いません。 ユニットテストは必須です。クラスまたはメソッドが必要な結果を得るために必要な多くの手順を記述します。



契約-クラス保証を顧客に説明し、ユニットテストでこれらの保証が満たされていることを確認します。



「落下」単体テストは、事後条件の違反と同様に、クラスコードのバグです。つまり、クラスのクライアントは事前条件の違反に遭遇することはありません。 クラス開発者は、クライアントによる前提条件の履行を保証することはできませんが、契約の一部を履行し、事後条件の履行を保証しようとすることはできます。 事後条件に違反した場合に発生する例外は、クライアントが使用してはならない保険です。 何らかの理由でメソッドがそのジョブを実行できない場合、適切なタイプの例外を使用してクライアントに明確に通知する必要があります。



単体テストとは異なり、契約はクライアントが常に利用でき、クライアントの助けを借りて、クラスまたはメソッドに何を期待するかをはるかに速く理解できます。 内部開発に関しても、コードの意図を記述するには契約が望ましい方法ですが、単体テストではその実現が保証されます。



実用的な例。 インターフェイスの契約




コントラクトプログラミング用の追加ライブラリの必要性は、ほとんどの現代言語の型システムがプログラマの意図を明確に表現していないという事実とも関連しています。 Eiffelまたは関数型言語のいずれかでnull不可能な参照型を自由に使用できる場合、引数チェックの数がどれほど少なくなるかを想像することができます。



型システムは、意図を伝えるための最初で最も重要な手段ですが、特にインターフェースに関しては、意図を表現することは非常に難しい場合があります。 インターフェースはいくつかの抽象化をモデル化し、各メソッドには特定のセマンティクスがあり、名前、引数セット、およびドキュメントを使用して理解できます。 インターフェイス仕様の他の同様に重要なソースはコントラクトであり、ある意味では単体テストです。

IListインターフェースのAddメソッドを見てみましょう。これは、かつてのいずれかのノートのメンバーでした:



//    [ContractClass(typeof(IListContract<>))] public interface IList<T> { /// <summary> /// Adds an item to the ICollection<T>. /// </summary> void Add(T item); int Count { get; } //     }
      
      







メソッドの名前、パラメーター、およびドキュメントだけを見ると、メソッドのどの事後条件の質問に明確に答えることは不可能です。 呼び出し元コードが期待できるもの、およびこのコントラクトを実装するクラスが提供するもの。 要素を1つだけ追加する必要があるかどうか。 契約を見ることは明らかですが、それなしでは理解することは非常に困難です。



 [ContractClassFor(typeof(IList<>))] internal abstract class IListContract<T> : IList<T> { void IList<T>.Add(T item) { Contract.Ensures(this.Count == (Contract.OldValue<int>(this.Count) + 1), "Count == Contract.OldValue(Count) + 1"); } }
      
      







もちろん、インターフェイスプロバイダーの単体テストはこの問題の解決に役立ちますが、ここでの問題は、さまざまなメーカーのインターフェイス実装が多数存在する可能性があるため、単体テストが仕様の最適なソースではないことです。



インターフェイスコントラクトは、特定のクラスメソッドの目的を理解する主な方法がインターフェイスでは機能しないため、特に役立ちます。 クラスメソッドの目的(セマンティクス)を理解するには、リバースエンジニアリングを使用しますが、このプロセスはすべての可能な実装を分析する必要があるため、インターフェイスでは複雑です。



コントラクトは、インターフェイスクライアントが使用できる追加情報を提供します。また、このインターフェイスを実装するクラスも重要です。



おわりに




契約とユニットテストは、仕様の表現として使用できますが、互いに競合するものではありません。 単体テストには多くの心配があります。実際のシステムではかなりの数があり、仕様の要素をそこから食い止めることは可能ですが、それほど簡単ではありません。 意図した目的のためにあらゆるツールを使用する必要があり、私たちの2人のヒーローも例外ではありません。



契約-抽象化を記述し、それがどのように配置されるかについて何も言わない。 単体テストでは、実装がこの説明に準拠していること、および契約に記載されている保証が常に満たされていることを確認します。



All Articles