命名模式的缺点:
1.文字拼写错误导致失败,测试方法没有执行,也没有报错 (JUNIT测试框架测试的方法要用test开头)
2.无法确保它们只用于相应的程序元素上,如希望一个类的所有方法被测试,把类命名为test开头,但JUnit不支持类级的测试,只在test开头的方法中生效
3.没有提供将参数值与程序元素关联起来的好方法。想要支持一种测试类别,它只在抛出特殊异常时才会成功。异常类型本质是测试的一个参数,如果命名类不存在,或者不是一个异常,你只有通过运行后才能发现。
注解能解决命名模式存在的问题,下面定义一个注解类型指定简单的测试,它们自动运行,并在抛出异常时失败(注意,下面的Test注解是自定义的,不是JUnit的实现)
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
像test使用了Retention和Target 这两种注解,这种注解被称为元注解
@Retention(RetentionPolicy.RUNTIME)表明Test注解在运行时保留,如果没有保留,测试工具无法知道Test注解
@Target(ElementType.METHOD)表明只有在方法声明中Test注解才是合法的,它不能运用到类声明,域声明或者其他程序元素上。
use only on parameterless static 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() {
thrownew RuntimeException("Crash");
}
public static void m8() {
}
}
在Sample 中有八个方法(其中m5不是静态方法),四个被注解为测试的方法中,有两个抛出异常:m3和m7,另外两个没有:m1和m5,被注解方法m5是一个实例方法,因此不属于注解的有效使用。没有进行标记的方法则会被测试工具忽略
test注解对Sample类的语义没有直接影响,只负责提供信息供相关程序使用。也就是注解不会改变被注解代码的语义,但是它可以通过工具进行特殊的处理。比如咱们用注解对方法进行简单的测试。
测试Sample的测试运行类:
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName("service.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);
}
}
测试运行工具在命令行上使用完全匹配的类名,并通过调用Method.invoke反射式的运行类中所有标注了test的方法,isAnnotationPresent 方法告知工具要运行哪些方法。如果测试方法抛出异常反射机制就会将错误信息封装到InvocationTargetException中,该工具捕捉到了这个异常,并且打印失败报告,包含测试方法抛出的原始异常,这些信息是通过getCasuse方法从InvocationTargetException中提取出来的
如果尝试通过反射调用测试方法时抛出InvocationTargetException之外的任何异常,表面编译的时候没有捕捉到Test注解的无效用法,这种用法包括实例方法的注解,或者带一个或者多个参数的方法的注解,并且打印相应的错误信息
运行结果:
public static void Sample.m3()
failed: java.lang.RuntimeException: Boom
INVALID @Test:public void Sample.m5()
public static void 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();
}
Sample --
public class Sample1 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
}
public static void m2() {
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {
throw new RuntimeException("Boom");
}
public static void m4() {
}
@ExceptionTest(ArithmeticException.class)
public void m5() {
}
public static void m6() {
}
@ExceptionTest(ArithmeticException.class)
public static void m7() {
throw new RuntimeException("Crash");
}
public static void m8() {
}
这段代码类似于用来处理Test注解的代码,但有一处不同:这段代码提取了注解参数的值,并用它检验该测试抛出的异常是否是正确的类型。没有显示的转换,因此没有出现类型转换异常的危险,编译过的测试程序确保它的注解参数表示的是有效的异常类型,需要提醒一点:有可能注解参数参数在编译时是有效的,但是表示特定异常类型的类文件在运行时却不再存在,在这种希望很少出现的情况下,测试运行类会抛出TypeNotPresentException异常。
将上面的异常测试示例再深入一点,测试可以抛出任何一种指定异常时都得到通过。我们将exceptionTest注解的参数类型改为Class对象的一个数组:
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 ExceptionTest1 {
Class <? extends Exception> [] value();
}
注解中数组参数的语法十分灵活。它是进行过优化的单元数组。使用了ExceptionTest新版的数组参数之后,之前的所有的ExceptionTest注解依然有效,并产生单元素包围起来,为了指定多元素的数组,需要用({})将元素保卫起来,并且用{,}隔开
public class Sample2 {
@ExceptionTest1({ArithmeticException.class,NullPointerException.class})
public static void m1() {
}
public static void m2() {
}
@ExceptionTest1({ArithmeticException.class,NullPointerException.class})
public static void m3() {
throw new RuntimeException("Boom");
}
public static void m4() {
}
@ExceptionTest1({ArithmeticException.class,NullPointerException.class})
public void m5() {
}
public static void m6() {
}
@ExceptionTest1({ArithmeticException.class,NullPointerException.class})
public static void m7() {
throw new RuntimeException("Crash");
}
public static void m8() {
}
}
public class RunTests2 {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName("service.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest1.class)) {
tests++;
try { //反射式的运行所有标注了Test的方法
m.invoke(null);
} catch (InvocationTargetException e) {
//InvocationTargetException异常由Method.invoke(obj, args...)方法抛出。当被调用的方法的内部抛出了异常而没有被捕获时,将由此异常接收。
Throwable exc = e.getCause();
Class[] excTypes = m.getAnnotation(ExceptionTest1.class).value();
int oldPassed = passed;
for (Class excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed) {
System.out.printf("测试%s失败:%s %n", m, exc);
}
} catch (Exception e) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
以上的例子不揭露了注解的冰山一角 , 但它鲜明了表达了一个观点 , 既然有了注解 , 就不必再用命名模式了
总结:除了特定的程序员之外 , 大多数程序员都不必定义注解类型 . 但是所有的程序员都应该使用Java平台所提供的预定义的注解类型 . 还要考虑 IDE(集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具) 或者静态分析工具所提供的任何注解 . 这种注解可以提升由这些工具所提供的诊断信息的质量 . 但是要注意这些注解还没有标准化 , 因此如果变换工具或者形成标准 , 就需要做更多地工作 .
。