企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
<!-- Annotation-Based Unit Testing --> ## 基于注解的单元测试 单元测试是对类中每个方法提供一个或者多个测试的一种事件,其目的是为了有规律的测试一个类中每个部分是否具备正确的行为。在 Java 中,最著名的单元测试工具就是 **JUnit**。**JUnit** 4 版本已经包含了注解。在注解版本之前的 JUnit 一个最主要的问题是,为了启动和运行 **JUnit** 测试,有大量的“仪式”需要标注。这种负担已经减轻了一些,**但是**注解使得测试更接近“可以工作的最简单的测试系统”。 在注解版本之前的 JUnit,你必须创建一个单独的文件来保存单元测试。通过注解,我们可以将单元测试集成在需要被测试的类中,从而将单元测试的时间和麻烦降到了最低。这种方式有额外的好处,就是使得测试私有方法和公有方法变的一样容易。 这个基于注解的测试框架叫做 **@Unit**。其最基本的测试形式,可能也是你使用的最多的一个注解是 **@Test**,我们使用 **@Test** 来标记测试方法。测试方法不带参数,并返回 **boolean** 结果来说明测试方法成功或者失败。你可以任意命名它的测试方法。同时 **@Unit** 测试方法可以是任意你喜欢的访问修饰方法,包括 **private**。 要使用 **@Unit**,你必须导入 **onjava.atunit** 包,并且使用 **@Unit** 的测试标记为合适的方法和字段打上标签(在接下来的例子中你会学到),然后让你的构建系统对编译后的类运行 **@Unit**,下面是一个简单的例子: ```java // annotations/AtUnitExample1.java // {java onjava.atunit.AtUnit // build/classes/main/annotations/AtUnitExample1.class} package annotations; import onjava.atunit.*; import onjava.*; public class AtUnitExample1 { public String methodOne() { return "This is methodOne"; } public int methodTwo() { System.out.println("This is methodTwo"); return 2; } @Test boolean methodOneTest() { return methodOne().equals("This is methodOne"); } @Test boolean m2() { return methodTwo() == 2; } @Test private boolean m3() { return true; } // Shows output for failure: @Test boolean failureTest() { return false; } @Test boolean anotherDisappointment() { return false; } } ``` 输出为: ```java annotations.AtUnitExample1 . m3 . methodOneTest . m2 This is methodTwo . failureTest (failed) . anotherDisappointment (failed) (5 tests) >>> 2 FAILURES <<< annotations.AtUnitExample1: failureTest annotations.AtUnitExample1: anotherDisappointment ``` 使用 **@Unit** 进行测试的类必须定义在某个包中(即必须包括 **package** 声明)。 **@Test** 注解被置于 `methodOneTest()`、 `m2()`、`m3()`、`failureTest()` 以及 `anotherDisappointment()` 方法之前,它们告诉 **@Unit** 方法作为单元测试来运行。同时 **@Test** 确保这些方法没有任何参数并且返回值为 **boolean** 或者 **void**。当你填写单元测试时,唯一需要做的就是决定测试是成功还是失败,(对于返回值为 **boolean** 的方法)应该返回 **ture** 还是 **false**。 如果你熟悉 **JUnit**,你还将注意到 **@Unit** 输出的信息更多。你会看到现在正在运行的测试的输出更有用,最后它会告诉你导致失败的类和测试。 你并非必须将测试方法嵌入到原来的类中,有时候这种事情根本做不到。要生产一个非嵌入式的测试,最简单的方式就是继承: ```java // annotations/AUExternalTest.java // Creating non-embedded tests // {java onjava.atunit.AtUnit // build/classes/main/annotations/AUExternalTest.class} package annotations; import onjava.atunit.*; import onjava.*; public class AUExternalTest extends AtUnitExample1 { @Test boolean _MethodOne() { return methodOne().equals("This is methodOne"); } @Test boolean _MethodTwo() { return methodTwo() == 2; } } ``` 输出为: ```java annotations.AUExternalTest . tMethodOne . tMethodTwo This is methodTwo OK (2 tests) ``` 这个示例还表现出灵活命名的价值。在这里,**@Test** 方法被命名为下划线前缀加上要测试的方法名称(我并不认为这是一种理想的命名形式,这只是表现一种可能性罢了)。 你也可以使用组合来创建非嵌入式的测试: ```java // annotations/AUComposition.java // Creating non-embedded tests // {java onjava.atunit.AtUnit // build/classes/main/annotations/AUComposition.class} package annotations; import onjava.atunit.*; import onjava.*; public class AUComposition { AtUnitExample1 testObject = new AtUnitExample1(); @Test boolean tMethodOne() { return testObject.methodOne() .equals("This is methodOne"); } @Test boolean tMethodTwo() { return testObject.methodTwo() == 2; } } ``` 输出为: ```java annotations.AUComposition . tMethodTwo This is methodTwo . tMethodOne OK (2 tests) ``` 因为在每一个测试里面都会创建 **AUComposition** 对象,所以创建新的成员变量 **testObject** 用于以后的每一个测试方法。 因为 **@Unit** 中没有 **JUnit** 中特殊的 **assert** 方法,不过另一种形式的 **@Test** 方法仍然允许返回值为 **void**(如果你还想使用 **true** 或者 **false** 的话,也可以使用 **boolean** 作为方法返回值类型)。为了表示测试成功,可以使用 Java 的 **assert** 语句。Java 断言机制需要你在 java 命令行行加上 **-ea** 标志来开启,但是 **@Unit** 已经自动开启了该功能。要表示测试失败的话,你甚至可以使用异常。**@Unit** 的设计目标之一就是尽可能减少添加额外的语法,而 Java 的 **assert** 和异常对于报告错误而言,即已经足够了。一个失败的 **assert** 或者从方法从抛出的异常都被视为测试失败,但是 **@Unit** 不会在这个失败的测试上卡住,它会继续运行,直到所有测试完毕,下面是一个示例程序: ```java // annotations/AtUnitExample2.java // Assertions and exceptions can be used in @Tests // {java onjava.atunit.AtUnit // build/classes/main/annotations/AtUnitExample2.class} package annotations; import java.io.*; import onjava.atunit.*; import onjava.*; public class AtUnitExample2 { public String methodOne() { return "This is methodOne"; } public int methodTwo() { System.out.println("This is methodTwo"); return 2; } @Test void assertExample() { assert methodOne().equals("This is methodOne"); } @Test void assertFailureExample() { assert 1 == 2: "What a surprise!"; } @Test void exceptionExample() throws IOException { try(FileInputStream fis = new FileInputStream("nofile.txt")) {} // Throws } @Test boolean assertAndReturn() { // Assertion with message: assert methodTwo() == 2: "methodTwo must equal 2"; return methodOne().equals("This is methodOne"); } } ``` 输出为: ```java annotations.AtUnitExample2 . exceptionExample java.io.FileNotFoundException: nofile.txt (The system cannot find the file specified) (failed) . assertExample . assertAndReturn This is methodTwo . assertFailureExample java.lang.AssertionError: What a surprise! (failed) (4 tests) >>> 2 FAILURES <<< annotations.AtUnitExample2: exceptionExample annotations.AtUnitExample2: assertFailureExample ``` 如下是一个使用非嵌入式测试的例子,并且使用了断言,它将会对 **java.util.HashSet** 进行一些简单的测试: ```java // annotations/HashSetTest.java // {java onjava.atunit.AtUnit // build/classes/main/annotations/HashSetTest.class} package annotations; import java.util.*; import onjava.atunit.*; import onjava.*; public class HashSetTest { HashSet<String> testObject = new HashSet<>(); @Test void initialization() { assert testObject.isEmpty(); } @Test void _Contains() { testObject.add("one"); assert testObject.contains("one"); } @Test void _Remove() { testObject.add("one"); testObject.remove("one"); assert testObject.isEmpty(); } } ``` 采用继承的方式可能会更简单,也没有一些其他的约束。 对每一个单元测试而言,**@Unit** 都会使用默认的无参构造器,为该测试类所属的类创建出一个新的实例。并在此新创建的对象上运行测试,然后丢弃该对象,以免对其他测试产生副作用。如此创建对象导致我们依赖于类的默认构造器。如果你的类没有默认构造器,或者对象需要复杂的构造过程,那么你可以创建一个 **static** 方法专门负责构造对象,然后使用 **@TestObjectCreate** 注解标记该方法,例子如下: ```java // annotations/AtUnitExample3.java // {java onjava.atunit.AtUnit // build/classes/main/annotations/AtUnitExample3.class} package annotations; import onjava.atunit.*; import onjava.*; public class AtUnitExample3 { private int n; public AtUnitExample3(int n) { this.n = n; } public int getN() { return n; } public String methodOne() { return "This is methodOne"; } public int methodTwo() { System.out.println("This is methodTwo"); return 2; } @TestObjectCreate static AtUnitExample3 create() { return new AtUnitExample3(47); } @Test boolean initialization() { return n == 47; } @Test boolean methodOneTest() { return methodOne().equals("This is methodOne"); } @Test boolean m2() { return methodTwo() == 2; } } ``` 输出为: ```java annotations.AtUnitExample3 . initialization . m2 This is methodTwo . methodOneTest OK (3 tests) ``` **@TestObjectCreate** 修饰的方法必须声明为 **static** ,且必须返回一个你正在测试的类型对象,这一切都由 **@Unit** 负责确保成立。 有的时候,你需要向单元测试中增加一些字段。这时候可以使用 **@TestProperty** 注解,由它注解的字段表示只在单元测试中使用(因此,在你将产品发布给客户之前,他们应该被删除)。在下面的例子中,一个 **String** 通过 `String.split()` 方法进行分割,从其中读取一个值,这个值将会被生成测试对象: ```java // annotations/AtUnitExample4.java // {java onjava.atunit.AtUnit // build/classes/main/annotations/AtUnitExample4.class} // {VisuallyInspectOutput} package annotations; import java.util.*; import onjava.atunit.*; import onjava.*; public class AtUnitExample4 { static String theory = "All brontosauruses " + "are thin at one end, much MUCH thicker in the " + "middle, and then thin again at the far end."; private String word; private Random rand = new Random(); // Time-based seed public AtUnitExample4(String word) { this.word = word; } public String getWord() { return word; } public String scrambleWord() { List<Character> chars = Arrays.asList( ConvertTo.boxed(word.toCharArray())); Collections.shuffle(chars, rand); StringBuilder result = new StringBuilder(); for(char ch : chars) result.append(ch); return result.toString(); } @TestProperty static List<String> input = Arrays.asList(theory.split(" ")); @TestProperty static Iterator<String> words = input.iterator(); @TestObjectCreate static AtUnitExample4 create() { if(words.hasNext()) return new AtUnitExample4(words.next()); else return null; } @Test boolean words() { System.out.println("'" + getWord() + "'"); return getWord().equals("are"); } @Test boolean scramble1() { // Use specific seed to get verifiable results: rand = new Random(47); System.out.println("'" + getWord() + "'"); String scrambled = scrambleWord(); System.out.println(scrambled); return scrambled.equals("lAl"); } @Test boolean scramble2() { rand = new Random(74); System.out.println("'" + getWord() + "'"); String scrambled = scrambleWord(); System.out.println(scrambled); return scrambled.equals("tsaeborornussu"); } } ``` 输出为: ```java annotations.AtUnitExample4 . words 'All' (failed) . scramble1 'brontosauruses' ntsaueorosurbs (failed) . scramble2 'are' are (failed) (3 tests) >>> 3 FAILURES <<< annotations.AtUnitExample4: words annotations.AtUnitExample4: scramble1 annotations.AtUnitExample4: scramble2 ``` **@TestProperty** 也可以用来标记那些只在测试中使用的方法,但是它们本身不是测试方法。 如果你的测试对象需要执行某些初始化工作,并且使用完成之后还需要执行清理工作,那么可以选择使用 **static** 的 **@TestObjectCleanup** 方法,当测试对象使用结束之后,该方法会为你执行清理工作。在下面的示例中,**@TestObjectCleanup** 为每一个测试对象都打开了一个文件,因此必须在丢弃测试的时候关闭该文件: ```java // annotations/AtUnitExample5.java // {java onjava.atunit.AtUnit // build/classes/main/annotations/AtUnitExample5.class} package annotations; import java.io.*; import onjava.atunit.*; import onjava.*; public class AtUnitExample5 { private String text; public AtUnitExample5(String text) { this.text = text; } @Override public String toString() { return text; } @TestProperty static PrintWriter output; @TestProperty static int counter; @TestObjectCreate static AtUnitExample5 create() { String id = Integer.toString(counter++); try { output = new PrintWriter("Test" + id + ".txt"); } catch(IOException e) { throw new RuntimeException(e); } return new AtUnitExample5(id); } @TestObjectCleanup static void cleanup(AtUnitExample5 tobj) { System.out.println("Running cleanup"); output.close(); } @Test boolean test1() { output.print("test1"); return true; } @Test boolean test2() { output.print("test2"); return true; } @Test boolean test3() { output.print("test3"); return true; } } ``` 输出为: ```java annotations.AtUnitExample5 . test1 Running cleanup . test3 Running cleanup . test2 Running cleanup OK (3 tests) ``` 在输出中我们可以看到,清理方法会在每个测试方法结束之后自动运行。