第35条:注解优先于命名模式
Java 1.5之前,一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。例如,JUnit测试框架原本要求用户一定要用test作为测试方法名称的开头。
命名模式的缺点:1.文字拼写错误导致失败,测试方法没有执行,也没有报错 2.无法确保它们只用于相应的程序元素上,如希望一个类的所有方法被测试,把类命名为test开头,但JUnit不支持类级的测试,只在test开头的方法中生效 3.没有提供将参数值与程序元素关联起来的好方法。
注解能解决命名模式存在的问题,下面定义一个注解类型指定简单的测试,它们自动运行,并在抛出异常时失败(注意,下面的Test注解是自定义的,不是JUnit的实现)
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME)表明Test注解在运行时保留
@Target(ElementType.METHOD)表明只有在方法声明中Test注解才是合法的
下面的Sample类使用Test注解,如果拼错Test或者将Test注解应用到除方法外的其他地方,
编译不会通过
public class Sample { @Test public static void m1() { } public static void m2() { } @Test public static void m3() { throw new RuntimeException("Boom"); } public static void m4() { } @Test public void m5() { } public static void m6() { } @Test public static void m7() { throw new RuntimeException("Crash"); } public static void m8() { } }
测试Sample的测试运行类:
public class RunTests { public static void main(String[] args) throws ClassNotFoundException { int tests = 0; int passed = 0; Class testClass = Class.forName("ex_35.Sample"); for(Method m : testClass.getDeclaredMethods()) { if(m.isAnnotationPresent(Test.class)) { tests++; try { m.invoke(null); passed++; } catch(InvocationTargetException wrappedExc) { Throwable exc = wrappedExc.getCause(); System.out.println(m + " failed: " + exc); } catch(Exception e) { System.out.println("INVALID @Test: " + m); } } } System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed); } }
运行结果:
public static void ex_35.Sample.m3() failed: java.lang.RuntimeException: Boom INVALID @Test: public void ex_35.Sample.m5() public static void ex_35.Sample.m7() failed: java.lang.RuntimeException: Crash Passed: 1, Failed: 3
针对只有在抛出特殊异常才成功的注解:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Exception> value(); }
JUnit提供一个完整的测试框架(下面例子基于Junit4)
需要被测试的类:
public class Calculator { private static int result; // 静态变量,用于存储运行结果 public void add(int n) { result = result + n; } public void substract(int n) { result = result - 1; //Bug: 正确的应该是 result =result-n } public void multiply(int n) { } // 此方法尚未写好 public void divide(int n) { result = result / n; } public void square(int n) { result = n * n; } public void squareRoot(int n) { for (; ;) ; //Bug : 死循环 } public void clear() { // 将结果清零 result = 0; } public int getResult() { return result; } }
用于测试Calculator类的测试类:
public class CalculatorTest { private static Calculator calculator = new Calculator(); @Before public void setUp() throws Exception { calculator.clear(); } @Test public void testAdd() { calculator.add(2); calculator.add(3); assertEquals(5, calculator.getResult()); } @Test public void testSubstract() { calculator.add(10); calculator.substract(2); assertEquals(8, calculator.getResult()); } @Ignore("Multiply() Not yet implemented") @Test public void testMultiply() { //fail("Not yet implemented"); } @Test public void testDivide() { calculator.add(8); calculator.divide(2); assertEquals(4, calculator.getResult()); } @Test(timeout = 1000) public void squareRoot() { calculator.squareRoot(4); assertEquals( 2 , calculator.getResult()); } @Test(expected = ArithmeticException.class) public void divideByZero() { calculator.divide(0); } }
通过Junit Test来运行CalculatorTest,结果:
明确告诉我们哪些测试出错。
除了编写测试框架的程序员外,我们不需要定义注解类型。鼓励使用Java平台提供的预定义注解类型,保证跨平台性,考虑使用IDE或者静态分析工具,如Junit的图形界面,方便在测试出错修改。