TDDプラクティスの簡単な例でjunit-quickcheckを使用する

もう一度、JavaでTDDの練習をして、テストに合格してもコードが正しく実行されるという意味ではないことを示す小さなトレーニングプロジェクトを作成しました。 実際、さまざまな入力オプションを考慮した単体テストの開発は骨の折れる作業です。 この問題を解決するために、私は自分用にjunit-quickcheckライブラリを見つけました。



ちなみに、このライブラリに関するライブラリにはすでにすばらしい記事がありますが、テストしたコードの別の例を使用します。



最初のステップ



そこで、新しい空のJavaプロジェクトを作成し、アセンブリスクリプトを作成しました。 私はgradleを使用してこのプロジェクトを構築します。 私のトレーニングプロジェクトはこちらでご覧いただけます 。 このコミットから始まります:d9bd97eaec20427e725f9a4c3ff0c0d36cc27ea3プロジェクトはすでにテストケースをビルドして実装する準備ができています。



次に、TDDの実践に従ってコードの記述を開始する必要があります。 一部のTDD謝罪者は、このクラスのテストを作成して、クラスの開発を開始する必要があると考えています。 しかし、ここで私は彼らと議論します。 私のIDE(IntelliJ IDEA)は既製のクラスがあるときにテストを生成できるため、空のクラスを作成して開発を開始します



public class HelloSayer { }
      
      





以降、コードはgithubコミットへのリンクで示されます。



次に、IDEにテストを作成してもらい、テストファーストスキームの作業を開始します。 最初に行うテストは、新しいオブジェクトを作成するだけの非常に簡単なテストです。 コンストラクターで挨拶する相手を指定します。 さらに、あいさつを発行するオブジェクトがこのあいさつを発行できるのは1つだけだと思います。 したがって、このクラスにはパラメーターのないコンストラクターはありません。



これが私が作成し最初のテストです。



  @Test public void testCreating() throws Exception { new HelloSayer("World"); }
      
      





この時点でプロジェクトはコンパイルされないことに注意してください。つまり、テストは赤です。



さて、テストを緑色にします。 IDEは、作成したテストのように、既に使用しているクラスメソッド(コンストラクターを含む)を作成できますが、まだ実装していません。 1つのキーの組み合わせを使用して、 このコンストラクターを生成し、グリーンテストを取得します。



 public class HelloSayer { public HelloSayer(String whom) { } }
      
      





もちろん、テストは合格しますが、実際、私のクラスは私が望んだことをしません。 さらに取り組みます。



getWhomメソッドのテストを作成します -これはゲッターであり、このオブジェクトを使用してこのオブジェクトが挨拶を発行する相手を見つけることができます。



  @Test public void testWhomGetter() throws Exception { HelloSayer sayer = new HelloSayer("World"); assertEquals("World", sayer.getWhom()); }
      
      





プロジェクトは再びコンパイルされません。つまり、テストは再び赤になります。



続けましょう。

必要なゲッターを実装します。



 public class HelloSayer { private String whom; public HelloSayer(String whom) { } public String getWhom() { return whom; } }
      
      





プロジェクトは既にコンパイルされていますが、テストは失敗します。 テストに合格するにはコンストラクターを実際に実装する必要があります。以前の方法ではありません。



  public HelloSayer(String whom) { this.whom = whom; }
      
      





テストは再び緑色になります。



小さいながらも重要な説明。 このチュートリアルでは、非常に頻繁にリポジトリにコミットします。 これは、読者に私の手順の順序を示すために行います。 実際のプロジェクトで頻繁にコミットする必要はありません。



コミットは「アトミック」であると主張していますが。 アトミックコミットとは、たとえば、 seesparkbox.com / foundry / atomic_commits_with_gitで読むことができます。 Kratoの発言:あなたがしたことを正確に定式化できる限り、ある文ではこれはアトミックコミットです。



実装が正しくないグリーンテスト



さて、このクラスのメインメソッドであるウェルカムメッセージの受信を実装します。

テストは次のとおりです。



  @Test public void testGreetingString() throws Exception { HelloSayer sayer = new HelloSayer("World"); assertEquals("Hello \"World\"", sayer.getGreetingString()); }
      
      





挨拶をする相手の名前が引用されることを期待しています。 テストは赤です。



必要なメソッドを実装します。



  public String getGreetingString() { return "Hello \"World\""; }
      
      





テストは緑色です。 テストに簡単に合格できました! ちなみに、 getWhomメソッドを実装するとき私は同じことをすることができましたが、そのとき私は正直にすべてをしました、そして、ここで私はあまりにも面倒でした



したがって、問題があります。すべてのテストに合格しますが、クラスは必要なことを行いません。 ここで異議を申し立てることができます。このような問題は、トレーニングプロジェクトではなく、実際の生活で発生する可能性は低いです。 実際、ユニットテストは通常​​、機能を書くのと同じ人によって開発されます。 したがって、この人が要件をあまり満たしていないメソッドの実装を書いたのは非常に奇妙です。



しかし、第二に、さまざまな人がテストと機能を作成する場合があります。 そして、第一に、開発者がテストで可能なすべての入力オプションを予見しない場合がある、はるかに複雑なケースがあります。



実際、このような誤った実装を避けるには、別の行whoで 2回目のテストを行うだけで十分です。 しかし、私はこの問題を最も一般的なケースで解決しようとします。 一般的な形式の問題の解決には、多くの異なる行の使用が必要です。 つまり、私が行ったように世界の1つの行ではなく、別の行でテストを実行する必要があります。



一般的な問題を解決するために、サードパーティのライブラリjunit-quickcheckを使用しました: github.com/pholser/junit-quickcheckは 、JUnitの理論に基づいています。 プロジェクトに接続します。



  dependencies { testCompile 'junit:junit:4.+' + testCompile 'com.pholser:junit-quickcheck-core:0.5+' + testCompile 'com.pholser:junit-quickcheck-generators:0.5+' }
      
      





この記事の執筆時点では、ここで使用しているバージョン0.5はアルファステータスですが、このプロジェクトで使用するJava 8をサポートしていると主張しています。



このライブラリを使用して、 greetingStringメソッドをテストするテストメソッドを再実装しました



  @Theory public void greetingString( @ForAll String whom ) { HelloSayer sayer = new HelloSayer(whom); assertEquals(String.format("Hello \"%s\"", whom), sayer.getGreetingString()); }
      
      





このコードが何をするのか見てみましょう。 メソッドを使用した理論注釈は、それが理論に基づいたパラメーター化されたテストメソッドであることを示しています。 ForAllアノテーションは、このパラメーターが生成されることを示しています。 すぐに使用できるjunit-quickcheckは、文字列を含む多くのデータ型の値を生成できます。



テストを実行すると、必要に応じて赤になりました。 次に、 getGreetingStringメソッドの実装を修正します



  public String getGreetingString() { return String.format("Hello \"%s\"", whom); }
      
      





これでテストはグリーンになり、実際、実装は本来の状態になりました。 ブレークポイントを設定し、このメソッドに渡されるパラメーターをトレースすることをお勧めします。 私はそのような行を思い付かないでしょう。



次のステップでは、行生成を使用してすべてのテストを書き直しました

一般的に言えば、これも論点です。 おそらく、単純でわかりやすい文字列を使用した単純なテストを残しておくべきでした。 コードをデバッグする必要がある場合(たとえば、特定の入力行で予期しない動作が発生する場合)、簡単なテストを行う方が便利です。 テストコードが複数回呼び出されるため、ジェネレーターを使用した実装のデバッグはそれほど簡単ではありません。 必要な厳密に固定された文字列を使用するテストを実装する方がはるかに簡単です。 しかし、それにもかかわらず、私はすべてのテストを、生成されたパラメーターを持つテストに置き換えました。



このセクションの最後の仕上げとして、プロジェクトにコードカバレッジコントロールを追加しました 。 このJacocoに使用します。 私はそれを実行し、レポートを見て、コードの100%のカバレッジを楽しんでいます。



さまざまな実装のテスト



現在、「非常に重要な」 HelloSayerクラスの開発を始めています。 私はその実装が本当に好きではありません。

具体的には、getGreetingStringを実行するたびにString.formatが好きではありません。 ただし、問題ない場合もあれば、私に合わない場合もあります。



したがって、HelloSayerインターフェースをレンダリングし、それをいくつか実装します。 ここでも、IntelliJ IDEAのリファクタリング機能を利用しました。 それが私が結果として得たものです。 これで、HelloSayerがインターフェースになり、そこにあった実装がHelloSayerInplaceクラスに入りました。



インターフェースと実装
インターフェース:

 package info.risik.books.tdd.HelloWorld; public interface HelloSayer { String getWhom(); String getGreetingString(); }
      
      





実装:



 package info.risik.books.tdd.HelloWorld; public class HelloSayerInplace implements HelloSayer { private String whom; public HelloSayerInplace(String whom) { this.whom = whom; } @Override public String getWhom() { return whom; } @Override public String getGreetingString() { return String.format("Hello \"%s\"", whom); } }
      
      







次に、このインターフェイスの別の実装を作成します。これにより、コンストラクターに直接ウェルカムラインが作成され、オブジェクトに保存されます。 コピーアンドペーストと検索と置換のおかげでテストも実装しました。



インターフェイスの別の実装
HelloSayerAtOnceクラスの実装。



 package info.risik.books.tdd.HelloWorld; public class HelloSayerAtOnce implements HelloSayer { private String whom; private String message; public HelloSayerAtOnce(String whom) { this.whom = whom; this.message = String.format("Hello \"%s\"", whom); } @Override public String getWhom() { return whom; } @Override public String getGreetingString() { return message; } }
      
      





彼のユニットテスト:



 package info.risik.books.tdd.HelloWorld; import com.pholser.junit.quickcheck.ForAll; import junit.framework.TestCase; import org.junit.contrib.theories.Theories; import org.junit.contrib.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class HelloSayerAtOnceTest extends TestCase { @Theory public void testCreating( @ForAll String whom ) { new HelloSayerAtOnce(whom); } @Theory public void testWhomGetter( @ForAll String whom ) { HelloSayer sayer = new HelloSayerAtOnce(whom); assertEquals(whom, sayer.getWhom()); } @Theory public void greetingString( @ForAll String whom ) { HelloSayer sayer = new HelloSayerAtOnce(whom); assertEquals(String.format("Hello \"%s\"", whom), sayer.getGreetingString()); } }
      
      







ここに問題があります。 同じインターフェイスの別の3番目の実装が必要なので、同じコピーアンドペーストおよび検索と置換のメカニズムを使用して、3番目のテストクラスを作成する必要があります。 これは悪いです。 このインターフェイスを実装するすべてのクラスは、内部での実装方法に関係なく、テストで既に説明したのと同じ規則を満たしている必要があります。 私はDRYの原則を守りたい(自分を繰り返さないでください)。 つまり、このインターフェースのすべての可能な実装に同じテストを適用したいのです。



ここで、junit-quickcheckerが再び私の助けになりました。 変更されたテストは次のとおりです。



 @RunWith(Theories.class) public class HelloSayerTest { enum HelloSayerType { InPlace, AtOnce, } //... @Theory public void testWhomGetter( @ForAll String whom, @ForAll @ValuesOf HelloSayerType sayerType ) throws Exception { HelloSayer sayer = getFactory(sayerType, whom); assertEquals(whom, sayer.getWhom()); } //... private HelloSayer getFactory(HelloSayerType type, String whom) throws Exception { switch (type) { case InPlace: return new HelloSayerInplace(whom); case AtOnce: return new HelloSayerAtOnce(whom); } throw new Exception("Unknown HelloSayerType"); } }
      
      





テストメソッドに1つの生成されたパラメーターを追加しました:クラスの型(列挙として)。 junit-checkerの列挙型はそのまま使用できます。 これを行うには、@ ValuesOfアノテーションを追加するだけです。 実際、私がする必要があるのは、クラスの1つをインスタンス化するメソッドだけでした。



このパラメーターを使用して、すべてのテストメソッドを実装します。 新しいクラス用に別のテストクラスは必要ありません。 削除します。



まず、HelloSayerの3番目のバージョンを実装しています。



遅延文字列初期化を使用したHelloSayerの実装
 package info.risik.books.tdd.HelloWorld; public class HelloSayerLazy implements HelloSayer { private String whom; private String message; public HelloSayerLazy(String whom) { this.whom = whom; this.message = null; } @Override public String getWhom() { return whom; } @Override public String getGreetingString() { if (message == null) { makeMessage(); } return message; } private void makeMessage() { message = String.format("Hello \"%s\"", whom); } }
      
      







ここで、このクラスのすべてのテストを実装するには、3行だけ追加する必要があります。



  enum HelloSayerType { InPlace, AtOnce, + Lazy, } @Theory @@ -53,6 +54,8 @@ private HelloSayer getFactory(HelloSayerType type, String whom) throws Exception return new HelloSayerInplace(whom); case AtOnce: return new HelloSayerAtOnce(whom); + case Lazy: + return new HelloSayerLazy(whom); } throw new Exception("Unknown HelloSayerType"); }
      
      





コードカバレッジがまだ100%であることを確認します。 すべてが素晴らしい!



リンクおよびその他の必要な情報



Javaテストケースgithub.com/risik/tdd-book-java

元の記事のテキスト: github.com/risik/tdd-book/blob/master/helloworld/simple_test.ru.md

habrahabr.ru用に準備された記事のテキスト: github.com/risik/tdd-book/blob/master/helloworld/simple_test.ru.habr.txt

ライセンス: CC-BY-NC-SA



All Articles