ホームページ >Java >&#&チュートリアル >Java 単体テストを作成するための 7 つのヒント
テストは開発の非常に重要な側面であり、アプリケーションの運命を大きく左右します。優れたテストでは、アプリケーションのクラッシュの原因となる問題を早期に発見できますが、テストが不十分な場合は、常に失敗やダウンタイムが発生することがよくあります。
ソフトウェア テストには、単体テスト、機能テスト、統合テストの 3 つの主なタイプがありますが、このブログ投稿では、開発者レベルの単体テストについて説明します。詳細に入る前に、これら 3 つのテストのそれぞれの詳細を確認しましょう。
単体テストは、個々のコードコンポーネントをテストし、コードが期待どおりに動作することを確認するために使用されます。単体テストは開発者によって作成および実行されます。ほとんどの場合、JUnit や TestNG などのテスト フレームワーク が使用されます。通常、テスト ケースはメソッド レベルで記述され、自動化によって実行されます。
統合テストでは、システムが全体として機能しているかどうかを確認します。統合テストも開発者によって行われますが、単一のコンポーネントをテストするのではなく、コンポーネント全体をテストするように設計されています。システムは、コード、データベース、Web サーバーなどの多くの個別のコンポーネントで構成されます。統合テストでは、コンポーネントの配線、ネットワーク アクセス、データベースの問題などの問題を明らかにできます。
機能テストでは、指定された入力の結果を仕様と比較することで、各機能が正しく実装されているかどうかをチェックします。通常、これは開発者レベルではありません。機能テストは別のテスト チームによって実行されます。テスト ケースは仕様に基づいて作成され、実際の結果が期待される結果と比較されます。 Selenium や QTP など、自動機能テストに使用できるツールがいくつかあります。
前述したように、単体テストは開発者がコードが適切に動作しているかどうかを判断するのに役立ちます。このブログ投稿では、Java での単体テストに役立つヒントを提供します。
Java には単体テスト用のフレームワークがいくつか用意されています。 TestNG と JUnit は、最も人気のあるテスト フレームワークです。 JUnit と TestNG の優れた機能:
セットアップと実行が簡単。
サポートコメント。
特定のテストを無視またはグループ化して一緒に実行できます。
パラメータ化されたテスト、つまり実行時に異なる値を指定して単体テストを実行することをサポートします。
Ant、Maven、Gradle などのビルド ツールと統合することにより、自動テスト実行をサポートします。
EasyMock は、JUnit や TestNG などの単体テスト フレームワークを補完するモック フレームワークです。 EasyMock 自体は完全なフレームワークではありません。テストを容易にするためにモック オブジェクトを作成する機能が追加されるだけです。たとえば、テストしたいメソッドの 1 つは、データベースからデータを取得する DAO クラスを呼び出すことができます。この場合、EasyMock を使用して、ハードコードされたデータを返す MockDAO を作成できます。これにより、データベースへのアクセスを心配することなく、目的のメソッドを簡単にテストできます。
テスト駆動開発 (TDD) は、コーディングを開始する前に要件に基づいてテストを作成するソフトウェア開発プロセスです。まだコーディングが行われていないため、テストは最初は失敗します。次に、テストに合格するための最小限のコードを記述します。次に、最適化されるまでコードをリファクタリングします。
目標は、要件さえ満たさない可能性のあるコードを最初から書くのではなく、すべての要件をカバーするテストを書くことです。 TDD が優れているのは、保守が容易なシンプルな モジュラー コードが得られるためです。全体的な開発速度が向上し、欠陥が見つけやすくなります。さらに、単体テストは TDD アプローチの副産物として作成されます。
ただし、TDD はすべての状況に適しているわけではありません。複雑な設計のプロジェクトでは、先を考えずにテスト ケースを通過しやすくするために最も単純な設計に重点を置くと、コードが大幅に変更される可能性があります。さらに、TDD 手法は、レガシー システム、GUI アプリケーション、データベースと連携するアプリケーションと対話するシステムには使用が困難です。さらに、コードの変更に応じてテストを更新する必要があります。
したがって、TDD アプローチの採用を決定する前に、上記の要素を考慮し、プロジェクトの性質に応じて対策を講じる必要があります。
コード カバレッジは、単体テストの実行時に実行されるコードの量を測定します (パーセントで表されます)。一般に、カバレッジが高いコードは、テスト中により多くのソース コードが実行されるため、検出されないバグが含まれる可能性が低くなります。コード カバレッジを測定するためのベスト プラクティスには次のようなものがあります。
Clover、Corbetura、JaCoCo、Sonar などのコード カバレッジ ツールを使用します。ツールを使用すると、テストされていないコードの領域を指摘し、それらの領域をカバーする追加のテストを開発できるため、テストの品質を向上させることができます。
新しい機能が作成されるたびに、すぐに新しいテスト カバレッジを作成します。
コードのすべての分岐、つまり if/else ステートメントをカバーするテスト ケースがあることを確認してください。
コード カバレッジが高いからといって、完璧なテストが保証されるわけではないので、注意してください。
以下の concat
メソッドは入力としてブール値を受け入れ、ブール値が true の場合に 2 つの追加の 文字列 concat
方法接受布尔值作为输入,并且仅当布尔值为true时附加传递两个字符串:
public String concat(boolean append, String a,String b) { String result = null; If (append) { result = a + b; } return result.toLowerCase(); }
以下是上述方法的测试用例:
@Test public void testStringUtil() { String result = stringUtil.concat(true, "Hello ", "World"); System.out.println("Result is "+result); }
在这种情况下,执行测试的值为true。当测试执行时,它将通过。当代码覆盖率工具运行时,它将显示100%的代码覆盖率,因为concat
方法中的所有代码都被执行。但是,如果测试执行的值为false,则将抛出NullPointerException
。所以100%的代码覆盖率并不真正表明测试覆盖了所有场景,也不能说明测试良好。
在JUnit4之前,测试用例要运行的数据必须硬编码到测试用例中。这导致了限制,为了使用不同的数据运行测试,测试用例代码必须修改。但是,JUnit4以及TestNG支持外部化测试数据,以便可以针对不同的数据集运行测试用例,而无需更改源代码。
下面的MathChecker
类有方法可以检查一个数字是否是奇数:
public class MathChecker { public Boolean isOdd(int n) { if (n%2 != 0) { return true; } else { return false; } } }
以下是MathChecker类的TestNG测试用例:
public class MathCheckerTest { private MathChecker checker; @BeforeMethod public void beforeMethod() { checker = new MathChecker(); } @Test @Parameters("num") public void isOdd(int num) { System.out.println("Running test for "+num); Boolean result = checker.isOdd(num); Assert.assertEquals(result, new Boolean(true)); } }
以下是testng.xml(用于TestNG的配置文件),它具有要为其执行测试的数据:
<?xml version="1.0" encoding="UTF-8"?> <suite name="ParameterExampleSuite" parallel="false"> <test name="MathCheckerTest"> <classes> <parameter name="num" value="3"></parameter> <class name="com.stormpath.demo.MathCheckerTest"/> </classes> </test> <test name="MathCheckerTest1"> <classes> <parameter name="num" value="7"></parameter> <class name="com.stormpath.demo.MathCheckerTest"/> </classes> </test> </suite>
可以看出,在这种情况下,测试将执行两次,值3和7各一次。除了通过XML配置文件指定测试数据之外,还可以通过DataProvider注释在类中提供测试数据。
与TestNG类似,测试数据也可以外部化用于JUnit。以下是与上述相同MathChecker类的JUnit测试用例:
@RunWith(Parameterized.class) public class MathCheckerTest { private int inputNumber; private Boolean expected; private MathChecker mathChecker; @Before public void setup(){ mathChecker = new MathChecker(); } // Inject via constructor public MathCheckerTest(int inputNumber, Boolean expected) { this.inputNumber = inputNumber; this.expected = expected; } @Parameterized.Parameters public static Collection<Object[]> getTestData() { return Arrays.asList(new Object[][]{ {1, true}, {2, false}, {3, true}, {4, false}, {5, true} }); } @Test public void testisOdd() { System.out.println("Running test for:"+inputNumber); assertEquals(mathChecker.isOdd(inputNumber), expected); } }
可以看出,要对其执行测试的测试数据由getTestData()方法指定。此方法可以轻松地修改为从外部文件读取数据,而不是硬编码数据。
许多新手开发人员习惯于在每行代码之后编写System.out.println语句来验证代码是否正确执行。这种做法常常扩展到单元测试,从而导致测试代码变得杂乱。除了混乱,这需要开发人员手动干预去验证控制台上打印的输出,以检查测试是否成功运行。更好的方法是使用自动指示测试结果的断言。
下面的StringUti
类是一个简单类,有一个连接两个输入字符串并返回结果的方法:
public class StringUtil { public String concat(String a,String b) { return a + b; } }
以下是上述方法的两个单元测试:
@Test public void testStringUtil_Bad() { String result = stringUtil.concat("Hello ", "World"); System.out.println("Result is "+result); } @Test public void testStringUtil_Good() { String result = stringUtil.concat("Hello ", "World"); assertEquals("Hello World", result); }
testStringUtil_Bad将始终传递,因为它没有断言。开发人员需要手动地在控制台验证测试的输出。如果方法返回错误的结果并且不需要开发人员干预,则testStringUtil_Good将失败。
一些方法不具有确定性结果,即该方法的输出不是预先知道的,并且每一次都可以改变。例如,考虑以下代码,它有一个复杂的函数和一个计算执行复杂函数所需时间(以毫秒为单位)的方法:
public class DemoLogic { private void veryComplexFunction(){ //This is a complex function that has a lot of database access and is time consuming //To demo this method, I am going to add a Thread.sleep for a random number of milliseconds try { int time = (int) (Math.random()*100); Thread.sleep(time); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public long calculateTime(){ long time = 0; long before = System.currentTimeMillis(); veryComplexFunction(); long after = System.currentTimeMillis(); time = after - before; return time; } }
在这种情况下,每次执行calculateTime
:
この場合、実行されたテストの値は true です。テストが実行されると、テストは成功します。コード カバレッジ ツールを実行すると、concat
メソッド内のすべてのコードが実行されたため、100% のコード カバレッジが表示されます。ただし、値 false を指定してテストを実行すると、NullPointerException
がスローされます。したがって、100% のコード カバレッジは、実際にはテストがすべてのシナリオをカバーしていることを意味するわけではなく、テストが良好であることを意味するわけでもありません。
JUnit4 が登場する前は、実行するテスト ケースのデータをテスト ケースにハードコーディングする必要がありました。その結果、異なるデータでテストを実行するにはテスト ケース コードを変更する必要があるという制限が生じました。ただし、JUnit4 と TestNG はテスト データの外部化をサポートしているため、さまざまな データセットをターゲットにすることができます テストの実行ソースコードを変更しない場合。
次のMathChecker
クラスには、数値が奇数かどうかをチェックするメソッドがあります: rrreee
TestNGrrreee
ご覧のとおり、この場合、テストは値 3 と 7 に対して 1 回ずつ、計 2 回実行されます。 XML 構成ファイルを介してテスト データを指定するだけでなく、DataProvider アノテーションを介してクラスでテスト データを提供することもできます。StringUti
クラスは、次の 2 つの メソッドを接続する単純なクラスです。 🎜 文字列で 🎜 を入力し、結果を返します: 🎜rrreee 🎜 上記のメソッドの 2 つの単体テストを次に示します: 🎜rrreee 🎜 testStringUtil_Bad はアサーションがないため、常にパスされます。開発者は、コンソール上のテスト出力を手動で確認する必要があります。 testStringUtil_Good は、メソッドが間違った結果を返し、開発者の介入を必要としない場合には失敗します。 🎜🎜6. 決定的な結果を含むテストを構築する🎜🎜一部のメソッドには決定的な結果がありません。つまり、メソッドの出力は事前に不明であり、毎回変わる可能性があります。たとえば、次のコードについて考えてみましょう。このコードには、複雑な 関数 🎜 と、それにかかる時間の計算が含まれています。複雑な関数 (ミリ秒単位) メソッドを実行します: 🎜rrreee🎜 この場合、calculateTime
メソッドが実行されるたびに、異なる値が返されます。このメソッドの出力は可変であるため、このメソッドのテスト ケースを作成しても役に立ちません。したがって、テスト メソッドでは特定の実行の出力を検証できません。 🎜🎜 7. ポジティブなシナリオに加えて、ネガティブなシナリオとエッジケースをテストします 🎜🎜 通常、開発者は、アプリケーションが期待どおりに動作することを確認するために、テスト ケースの作成に多くの時間と労力を費やします。ただし、陰性のテストケースをテストすることも重要です。ネガティブ テスト ケースは、システムが無効なデータを処理できるかどうかをテストするテスト ケースを指します。たとえば、ユーザーが入力した長さ 8 の英数字値を読み取る単純な関数を考えてみましょう。英数字の値に加えて、次のネガティブ テスト ケースもテストする必要があります: 🎜🎜🎜🎜ユーザー指定の非英数字値 (🎜特殊文字🎜など)。 🎜🎜🎜🎜ユーザー指定の null 値。 🎜🎜🎜🎜8 文字より大きいまたは小さいユーザー指定の値。 🎜🎜🎜🎜同様に、境界テスト ケースでは、システムが極値に対して機能するかどうかをテストします。たとえば、ユーザーが 1 から 100 までの数値を入力したい場合、1 と 100 が境界値であり、これらの値についてシステムをテストすることが非常に重要です。 🎜以上がJava 単体テストを作成するための 7 つのヒントの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。