Javaでのテスト。 ユニット



今日、 テスト駆動開発(TDD)の人気が高まっています。これは、最初に特定の機能のテストを作成し、次にこの機能の実装を作成するソフトウェア開発手法です。 もちろん、実際には、すべてがそれほど完璧ではありませんが、結果として、コードは書かれてテストされるだけでなく、テストは機能の要件を暗黙的に設定し、この機能の使用例を示しているようです。



それで、テクニックはかなり明確ですが、問題は、これらのまさにテストを書くために何を使うべきですか? この記事や他の記事では、Javaでコードをテストするためのさまざまなツールとテクニックを使用した経験を共有したいと思います。



まず、おそらく最も有名な、したがって最もよく使用されるテストフレームワークであるJUnitから始めます 。 JUnit 3とJUnit 4の2つのバージョンで使用されます。古いプロジェクトではJava 1.4をサポートする3番目のバージョンがまだ使用されているため、両方のバージョンを検討します。



私は独創的なアイデアの作者のふりをするつもりはありません。おそらく、この記事で議論されることの多くはおなじみのものです。 それでも興味があれば、猫にようこそ。



ジュニット3



テストを作成するには、 TestCaseからテストクラスを継承し、必要に応じてsetUp メソッドtearDownメソッドを再定義し、最も重要なこととして、テストメソッドを作成する必要があります( testで開始する必要があります )。 テストが開始されると、最初にテストクラスのインスタンスが作成され(クラス内の各テストに個別のクラスのインスタンスがあります)、次にsetUpメソッドが実行され 、テスト自体が起動され、最後にtearDownメソッドが実行されます。 メソッドのいずれかが例外をスローした場合、テストは失敗したと見なされます。



注:テストメソッドはpublic voidである必要があり、 静的である場合があります



テスト自体は、いくつかのコードの実行とチェックで構成されています。 チェックはほとんどの場合Assertクラスを使用して実行されますが、時にはassertキーワードを使用します。



例を考えてみましょう。 文字列を操作するためのユーティリティがあり、空の文字列をチェックし、16進文字列としてバイトシーケンスを表すメソッドがあります。

public abstract class StringUtils { private static final int HI_BYTE_MASK = 0xf0; private static final int LOW_BYTE_MASK = 0x0f; private static final char[] HEX_SYMBOLS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', }; public static boolean isEmpty(final CharSequence sequence) { return sequence == null || sequence.length() <= 0; } public static String toHexString(final byte[] data) { final StringBuffer builder = new StringBuffer(2 * data.length); for (byte item : data) { builder.append(HEX_SYMBOLS[(HI_BYTE_MASK & item) >>> 4]); builder.append(HEX_SYMBOLS[(LOW_BYTE_MASK & item)]); } return builder.toString(); } }
      
      





JUnit 3を使用してテストを記述します。私の意見では、クラスをブラックボックスと見なしてテストを記述し、このクラスの重要なメソッドごとに個別のテストを記述し、入力パラメーターのセットごとにいくつかの期待される結果を作成するのが最も便利です。 たとえば、 isEmptyメソッドのテスト:

  public void testIsEmpty() { boolean actual = StringUtils.isEmpty(null); assertTrue(actual); actual = StringUtils.isEmpty(""); assertTrue(actual); actual = StringUtils.isEmpty(" "); assertFalse(actual); actual = StringUtils.isEmpty("some string"); assertFalse(actual); }
      
      





データ作成をsetUpメソッドに転送することにより、データとテストロジックを分離できます。

 public class StringUtilsJUnit3Test extends TestCase { private final Map toHexStringData = new HashMap(); protected void setUp() throws Exception { toHexStringData.put("", new byte[0]); toHexStringData.put("01020d112d7f", new byte[] { 1, 2, 13, 17, 45, 127 }); toHexStringData.put("00fff21180", new byte[] { 0, -1, -14, 17, -128 }); //... } protected void tearDown() throws Exception { toHexStringData.clear(); } public void testToHexString() { for (Iterator iterator = toHexStringData.keySet().iterator(); iterator.hasNext();) { final String expected = (String) iterator.next(); final byte[] testData = (byte[]) toHexStringData.get(expected); final String actual = StringUtils.toHexString(testData); assertEquals(expected, actual); } } //... }
      
      





追加機能



説明した内容に加えて、いくつかの追加機能があります。 たとえば、テストをグループ化できます。 これを行うには、 TestSuiteクラスを使用します。

 public class StringUtilsJUnit3TestSuite extends TestSuite { public StringUtilsJUnit3TestSuite() { addTestSuite(StringUtilsJUnit3Test.class); addTestSuite(OtherTest1.class); addTestSuite(OtherTest2.class); } }
      
      





同じテストを数回実行できます。 これを行うには、 RepeatedTestを使用します。

 public class StringUtilsJUnit3RepeatedTest extends RepeatedTest { public StringUtilsJUnit3RepeatedTest() { super(new StringUtilsJUnit3Test(), 100); } }
      
      





ExceptionTestCaseからテストクラスを継承すると、何かをチェックして例外をスローできます。

 public class StringUtilsJUnit3ExceptionTest extends ExceptionTestCase { public StringUtilsJUnit3ExceptionTest(final String name) { super(name, NullPointerException.class); } public void testToHexString() { StringUtils.toHexString(null); } }
      
      





例からわかるように、すべてが非常にシンプルであり、テストに最低限必要なものは何もありません(ただし、必要なものの一部は欠落しています)。



ユニット4



Java 5の新機能のサポートがここに追加され、アノテーションを使用してテストを宣言できるようになりました。 同時に、フレームワークの以前のバージョンとの下位互換性があり、上記のほとんどすべての例がここで機能します( RepeatedTestを除き、新しいバージョンにはありません)。



それで、何が変わったのでしょうか?



キー注釈



同じ例を考えますが、すでに新しい機能を使用しています:

 public class StringUtilsJUnit4Test extends Assert { private final Map<String, byte[]> toHexStringData = new HashMap<String, byte[]>(); @Before public static void setUpToHexStringData() { toHexStringData.put("", new byte[0]); toHexStringData.put("01020d112d7f", new byte[] { 1, 2, 13, 17, 45, 127 }); toHexStringData.put("00fff21180", new byte[] { 0, -1, -14, 17, -128 }); //... } @After public static void tearDownToHexStringData() { toHexStringData.clear(); } @Test public void testToHexString() { for (Map.Entry<String, byte[]> entry : toHexStringData.entrySet()) { final byte[] testData = entry.getValue(); final String expected = entry.getKey(); final String actual = StringUtils.toHexString(testData); assertEquals(expected, actual); } } }
      
      





ここで何が見えますか?



  @Test(expected = NullPointerException.class) public void testToHexStringWrong() { StringUtils.toHexString(null); } @Test(timeout = 1000) public void infinity() { while (true); }
      
      





何らかの深刻な理由でテストを無効にする必要がある場合(たとえば、このテストは常に失敗しますが、修正は明るい未来まで延期されます)、 @Ignoreアノテーションを付けることができます。 また、このアノテーションをクラスに配置すると、このクラスのすべてのテストが無効になります。

  @Ignore @Test(timeout = 1000) public void infinity() { while (true); }
      
      





ルール



上記のすべてに加えて、かなり興味深いものがあります-ルールです。 ルールは、テストの前後に機能を追加するテストの一種のユーティリティです。



たとえば、テストのタイムアウトの設定( Timeout )、 予期される例外の設定( ExpectedException )、一時ファイルの操作( TemporaryFolder )などの組み込みルールがあります。 ルールを宣言するには、 MethodRuleから派生したタイプのパブリック静的フィールドを作成し、 Ruleを使用して注釈を付ける必要があります。

 public class OtherJUnit4Test { @Rule public final TemporaryFolder folder = new TemporaryFolder(); @Rule public final Timeout timeout = new Timeout(1000); @Rule public final ExpectedException thrown = ExpectedException.none(); @Ignore @Test public void anotherInfinity() { while (true); } @Test public void testFileWriting() throws IOException { final File log = folder.newFile("debug.log"); final FileWriter logWriter = new FileWriter(log); logWriter.append("Hello, "); logWriter.append("World!!!"); logWriter.flush(); logWriter.close(); } @Test public void testExpectedException() throws IOException { thrown.expect(NullPointerException.class); StringUtils.toHexString(null); } }
      
      





また、ネットワーク上で他のユースケースを見つけることができます。 たとえば、テストを並行して実行する可能性をここで検討します



ランチャー



しかし、フレームワークの可能性はこれで終わりではありません。 テストの開始方法は、 @RunWithを使用して構成することもできます。 この場合、注釈で指定されたクラスはRunnerから継承する必要があります。 フレームワーク自体にバンドルされているスターターを検討してください。



JUnit4-名前が示すとおり、デフォルトのランチャーはJUnit 4テストを実行するように設計されています。



JUnit38ClassRunnerは 、JUnit 3を使用して記述されたテストを実行するように設計されています。



SuiteMethodまたはAllTestsは、 JUnit 3テストを実行するようにも設計されています。 前のランチャーとは異なり、このクラスには、テスト(すべてのテストのシーケンス)を返す静的メソッドスイートが渡されます。



Suiteは、以前のJUnit 4テストにのみ対応しています。 @SuiteClasses注釈は、実行中のテストを構成するために使用されます。

 @Suite.SuiteClasses( { OtherJUnit4Test.class, StringUtilsJUnit4Test.class }) @RunWith(Suite.class) public class JUnit4TestSuite { }
      
      





Enclosedは前のオプションと同じですが、アノテーションを使用して設定する代わりに、すべての内部クラスが使用されます。



カテゴリ -テストをカテゴリ(グループ)に整理する試み。 これを行うには、 @ Categoryを使用してカテゴリをテストに割り当て 、スイート内のテストの実行可能なカテゴリを構成します。 次のようになります。

 public class StringUtilsJUnit4CategoriesTest extends Assert { //... @Category(Unit.class) @Test public void testIsEmpty() { //... } //... } @RunWith(Categories.class) @Categories.IncludeCategory(Unit.class) @Suite.SuiteClasses( { OtherJUnit4Test.class, StringUtilsJUnit4CategoriesTest.class }) public class JUnit4TestSuite { }
      
      





パラメーター化は、パラメーター化されたテストを作成できる、かなり興味深いランチャーです。 これを行うには、データのリストを返す静的メソッドがテストクラスで宣言され、クラスコンストラクターへの引数として使用されます。

 @RunWith(Parameterized.class) public class StringUtilsJUnit4ParameterizedTest extends Assert { private final CharSequence testData; private final boolean expected; public StringUtilsJUnit4ParameterizedTest(final CharSequence testData, final boolean expected) { this.testData = testData; this.expected = expected; } @Test public void testIsEmpty() { final boolean actual = StringUtils.isEmpty(testData); assertEquals(expected, actual); } @Parameterized.Parameters public static List<Object[]> isEmptyData() { return Arrays.asList(new Object[][] { { null, true }, { "", true }, { " ", false }, { "some string", false }, }); } }
      
      





理論 -前のものに似ていますが、コンストラクタではなくテストメソッドをパラメータ化します。 データは@DataPoints@DataPointを使用してタグ付けされ 、テストメソッドはTheoryを使用してタグ付けされます。 この機能を使用したテストは次のようになります。

 @RunWith(Theories.class) public class StringUtilsJUnit4TheoryTest extends Assert { @DataPoints public static Object[][] isEmptyData = new Object[][] { { "", true }, { " ", false }, { "some string", false }, }; @DataPoint public static Object[] nullData = new Object[] { null, true }; @Theory public void testEmpty(final Object... testData) { final boolean actual = StringUtils.isEmpty((CharSequence) testData[0]); assertEquals(testData[1], actual); } }
      
      





ルールと同様に、他のユースケースもネット上で見つけることができます。 たとえば、 ここでは、ランチャーを使用してテストを並行して実行する同じ可能性を検討します。



おわりに



もちろん、これはJUnitで言えることのすべてではありませんが、私は簡単にビジネスで試しました。 ご覧のとおり、フレームワークは非常に使いやすく、追加機能はほとんどありませんが、ルールとランチャーを使用して拡張する可能性があります。 しかし、これにもかかわらず、私はまだ強力な機能を備えたTestNGを好みます。これについては、次の記事で説明します。



例はgithubにあります。



文学






All Articles