@test注解_JAVA编程思想(4)11注解

48bd60a216d0afb3c8cd360cc070a7c0.png

1. 注解

注解,也被称为元数据,为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。注解在Java SE5中引入,在一定程度是在把元数据与源代码文件结合在一起,而不是保存在外部文档中这一大趋势之下所催生的,可以联想到Spring早期的xml配置文件。

注解是真正的语言级的概念,一旦构造出来,就享有编译期的类型检查保护。注解是在实际的源代码级别保存所有的信息,而不是某种注释性的文字。通过使用扩展的annotation API,或外部的字节码工具类库,我们将拥有对源代码以及字节码强大的检查与操作能力。

注解的优点:

1.注解使我们能够用将会被编译器测试和验证的格式,来存储有关程序的额外信息2.注解可以用来生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。3.通过使用注解,我们可以将这些元数据保存在Java源代码中,并利用annotation API为自己的注解构造处理工具4.注解还能带来更加干净易读的代码,以及编译期的类型检查

注解的语法比较简单,除了@符号的使用之外,它基本上与Java固有的语法一致。Java SE5中内置了前三种标准注解,定义在java.lang中:

1.@Override: 表示当前的方法定义将覆盖超类中的方法。这个注解的使用是可选的,并且方法签名会受到编译期检查。2.@Deprecated: 表示被修饰的元素将在未来被废弃,如果这个元素被使用,编译器会发出警告信息。3.@SuppressWarnings: 关闭不当的编译器警告信息。4.@SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。5.@FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口。

1.1. 基本语法

1.1.1. 定义注解

首先来定义一个@Test:

package atunit;import java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Test { }

现在来使用这个@Test:

package annotations;import atunit.Test;public class Testable {    public void execute() {        System.out.println("Executing..");    }    @Test    void testExecute() {        execute();    }}

可以看到,注解的定义看起来很像接口的定义,并且注解也会被编译成class文件。定义注解时,会需要一些元注解(meta-annotation),例如@Target和@Retention。@Target用来定义你的注解将应用于什么地方,如方法上、域上;而@Retention用来定义该注解在哪一级别可用,如源代码中、类文件中、运行时。

在注解中,一般都会包含一些元素以表示某些值,这些元素看起来就像接口的方法,不过可以指定默认值。在分析处理注解时,程序或工具可以利用这些值。而没有元素的注解被称为标记注解,就像这个示例中的@Test。

下面再来定义一个注解,可以用它来跟踪一个项目中的用例:

package annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface UseCase {    public int id();    public String description() default "no description";}

在下面的类中,有三个方法被注解为用例:

package annotations;import java.util.List;public class PasswordUtils {    @UseCase(id=47, description = "...")    public boolean validatePassword(String password) {        return password.matches("\\w*\\d\\w*");    }    @UseCase(id=48)    public String encryptPassword(String password) {        return new StringBuilder(password).reverse().toString();    }    @UseCase(id=49, description = "...")    public boolean checkForNewPassword(            List prevPasswords, String password) {        return !prevPasswords.contains(password);    }}

注解的元素在使用时表现为名-值对的形式,并且需要置于@UseCase声明之后的括号内。

1.1.2. 元注解

Java中除了前面提到了三种标准注解,还提供了四种元注解,专职负责注解其他的注解。

注解名说明可选参数
@Target表示该注解可以用于什么地方CONSTRUCTOR:构造器、FIELD:域(包括enum实例)、 LOCAL_VARIABLE:局部变量、METHOD:方法、PACKAGE:包、PARAMETER:参数、TYPE:类、接口(包括注解类型)或enum
@Retention表示需要在什么级别保存该注解SOURCE:注解将被编译器丢弃、CLASS:注解在class文件中可用,但会被VM丢弃、RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息
@Documented将此注解包含在Javadoc中
@Inherited允许子类继承父类中的注解

1.2. 编写注解处理器

如果没有用来读取注解的工具,注解也不会比注释更有用。Java SE5扩展了反射机制的API来便于构造注解处理器,同时还提供了一个外部工具apt来助于解析带有注解的Java源代码,不过在Java 1.8中,apt被移除了,取而代之的是javac -processor。

下面是一个非常简单的注解处理器,我们将用它来读取PasswordUtils类,并用反射机制查找@UseCase标记。我们为其提供了一组id值,然后它会列出在PasswordUtils中找到的用例,以及缺失的用例:

package annotations;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.Collections;import java.util.List;public class UseCaseTracker {    public static void trackUseCases(List useCases, Class> clazz) {        for (Method method : clazz.getDeclaredMethods()) {            UseCase useCase = method.getAnnotation(UseCase.class);            if (useCase != null) {                System.out.println("Found Use Case: " + useCase.id());                useCases.remove(new Integer(useCase.id()));            }        }        for (int i : useCases) {            System.out.println("Warning: Missing use case: " + i);        }    }    public static void main(String[] args) {        List useCaseList = new ArrayList<>();        Collections.addAll(useCaseList, 47, 48, 49, 50);        trackUseCases(useCaseList, PasswordUtils.class);    }}/* Output:Found Use Case: 47Found Use Case: 48Found Use Case: 49Warning: Missing use case: 50*/

1.2.1. 注解元素

注解元素可用的类型有:

1.所有基本类型(int, float, boolean等)2.String3.Class4.enum5.Annotation6.以上类型的数组

注意,注解元素不允许使用包装类型,不过有自动包装机制的存在,这就算不上什么限制了。

1.2.2. 默认值限制

编译器对元素的默认值有严格的限制:

1.元素不能有不确定的值,要么具有默认值,要么在使用注解时提供元素的值。2.对于非基本类型的元素,无论是在源代码中声明时,还是注解接口中定义默认值时,都不能以null作为其值。

第二个约束使处理器很难表现一个元素存在或缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且都具有相应的值。为了绕开这个约束,我们可以定义空字符串或者负数来表示某个元素不存在。

1.2.3. 生成外部文件

有些framework需要一些额外信息才能与你的源代码协同工作,这种情况最适合注解了。

假设现在需要提供一些基本的对象/关系映射功能,能够自动生成数据库表,用以存储JavaBean对象。如果使用注解的话,可以将所有信息保存在JavaBean源文件中,为此我们需要定义一些新注解来定义与bean关联的数据库表的名字,以及与Bean属性关联的列的名字和SQL类型。

下面是一个注解的定义,它告诉注解处理器,你需要为我生成一个数据库表:

package annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface DBTable {    // 表的名字    public String name() default "";}

在@Target注解中指定的每个ElementType就是一个约束,如果想要指定多个值需要用逗号隔开,而如果想将注解应用于所有的ElementType,可以省略@Target元注解。

下面是修饰JavaBean域的三个注解:

// annotations/Constraints.javapackage annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface Constraints {    boolean primaryKey() default false;    boolean allowNull() default true;    boolean unique() default false;}// annotations/SQLString.javapackage annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface SQLString {    int value() default 0;    String name() default "";    Constraints constraints() default @Constraints;}// annotations/SQLInteger.javapackage annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface SQLInteger {    String name() default "";    Constraints constraints() default @Constraints;}

注解处理器通过@Constraints注解提取出数据库的元数据,后两个定义了两种SQL类型作为示例。如果想要让嵌入的@Constraints注解中的unique()元素为true,并以此作为constraints()元素的默认值,就需要像这样:Constraints constraint() default @Constraints(unique = true);

然后是一个简单的Bean定义,我们在其中应用了上面的注解:

package annotations;@DBTable(name = "MEMBER")public class Member {    @SQLString(30)    String firstName;    @SQLString(50)    String lastName;    @SQLInteger    Integer age;    @SQLString(value = 30, constraints = @Constraints(primaryKey = true))    String handle;    static int memberCount;}

在注解使用中,有一个快捷方式:如果在注解中定义了名为value的元素,并且在应用该注解时,value是唯一需要赋值的元素,就可以直接在括号中给出元素所需要的值。这种使用方式要求元素只能命名为value。

1.2.4. 注解不支持继承

不能使用关键字extends来继承某个注解。至少目前,Java语法上没有实现注解的继承机制。

1.2.5. 实现处理器

下面是一个注解处理器的例子,它将读取一个类文件,检查其上的数据注解,并生成用来创建数据库的SQL命令。

package annotations;import java.lang.annotation.Annotation;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.List;public class TableCreator {    public static void main(String[] args) throws Exception {        String className = "annotations.Member";        Class> clazz = Class.forName(className);        DBTable dbTable = clazz.getAnnotation(DBTable.class);        if (dbTable == null) {            System.out.println("No DBTable annotations in class " + className);        }        String tableName = dbTable.name();        if (tableName.length() < 1) {            tableName = clazz.getName().toUpperCase();        }        // 保存每一列的SQL语句        List columnDefs = new ArrayList<>();        for (Field field : clazz.getDeclaredFields()) {            String columnName = null;            Annotation[] annotations = field.getDeclaredAnnotations();            // 这个字段上没有注解            if (annotations.length < 1) {                continue;            }            // 在这个示例中,字段上只有一个注解            if (annotations[0] instanceof SQLInteger) {                SQLInteger sqlInteger = (SQLInteger) annotations[0];                // 如果name元素没有指定,就使用字段名                if (sqlInteger.name().length() < 1) {                    columnName = field.getName().toUpperCase();                } else {                    className = sqlInteger.name();                }                columnDefs.add(columnName + " INT" + getConstraints(sqlInteger.constraints()));            }            if (annotations[0] instanceof SQLString) {                SQLString sqlString = (SQLString) annotations[0];                if (sqlString.name().length() < 1) {                    columnName = field.getName().toUpperCase();                } else {                    columnName = sqlString.name();                }                columnDefs.add(columnName + " VARCHAR(" + sqlString.value() + ")" + getConstraints(sqlString.constraints()));            }        }        StringBuilder stringBuilder = new StringBuilder("CREATE TABLE " + tableName + "(");        for (String columnDef : columnDefs) {            stringBuilder.append("\n    ").append(columnDef).append(",");        }        // 去掉最后一个语句跟着的逗号        String tableCreated = stringBuilder.substring(0, stringBuilder.length() - 1) + ");";        System.out.println("Table Creation SQL for " + className + " is :\n" + tableCreated);    }    private static String getConstraints(Constraints cons) {        String constraints = "";        if (!cons.allowNull()) {            constraints += " NOT NULL";        }        if (cons.primaryKey()) {            constraints += " PRIMARY KEY";        }        if (cons.unique()) {            constraints += " UNIQUE";        }        return constraints;    }}/* Output:Table Creation SQL for annotations.Member is :CREATE TABLE MEMBER(    FIRSTNAME VARCHAR(30),    LASTNAME VARCHAR(50),    AGE INT,    HANDLE VARCHAR(30) PRIMARY KEY);*/

1.3. 使用javac处理注解

在Java SE5中,注解处理工具apt是为了帮助注解的处理过程而提供的工具,不过apt在Java8中被移除了。

不过我们可以通过javac来创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重大限制:你不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。

如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它会编译所有的源文件。

每一个你编写的注解都需要处理器,但是javac可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。

1.3.1. 最简单的处理器

我们来定义一个最简单的处理器,只是为了编译与测试用。首先是注解的定义:

package annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.SOURCE)@Target({ElementType.TYPE, ElementType.METHOD,        ElementType.CONSTRUCTOR,        ElementType.ANNOTATION_TYPE,        ElementType.PACKAGE, ElementType.FIELD,        ElementType.LOCAL_VARIABLE})public @interface Simple {    String value() default "-default-";}

@Retention的参数现在为SOURCE,这意味着注解不会存留在编译后的代码。这在编译时处理注解是没有必要的,它只是指出,在这里,javac是唯一有机会处理注解的代理。@Target声明了几乎所有的目标类型(除了PACKAGE),同样是为了演示。

现在来使用这个注解:

package annotations;@Simplepublic class SimpleTest {    @Simple    private int i;    @Simple    public SimpleTest() {    }    @Simple    public void foo() {        System.out.println("SimpleTest.foo()");    }    @Simple    public void bar(String s, int i, float f) {        System.out.println("SimpleTest.bar()");    }    @Simple    public static void main(String[] args) {        @Simple        SimpleTest simpleTest = new SimpleTest();        simpleTest.foo();        simpleTest.bar("", 0, 0f);    }}

在这里我们使用@Simple注解了所有@Target声明允许的地方。SimpleTest.java只需要Simple.java就可以编译成功。

不过当我们编译的时候什么都没有发生,因为javac允许@Simple注解在我们创建处理器并将其hook到编译器之前,不做任何事情。

如下是一个十分简单的处理器,它的功能就是把注解相关的信息打印出来:

package annotations;import javax.annotation.processing.*;import javax.lang.model.SourceVersion;import javax.lang.model.element.*;import java.util.Set;@SupportedAnnotationTypes("annotations.Simple")@SupportedSourceVersion(SourceVersion.RELEASE_8)public class SimpleProcessor extends AbstractProcessor {    @Override    public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {        // 这里只有一个注解        for (TypeElement typeElement : annotations) {            System.out.println(typeElement);        }        for (Element element : roundEnv.getElementsAnnotatedWith(Simple.class)) {            display(element);        }        return false;    }    private void display(Element element) {        System.out.println("====== " + element + " ======");        System.out.println(element.getKind() + ":"                + element.getModifiers() + ":"                + element.getSimpleName() + ":"                + element.asType());        if(element.getKind().equals(ElementKind.CLASS)){            TypeElement typeElement = (TypeElement)element;            System.out.println(typeElement.getQualifiedName());            System.out.println(typeElement.getSuperclass());            System.out.println(typeElement.getEnclosedElements());        }        if(element.getKind().equals(ElementKind.METHOD)){            ExecutableElement executableElement = (ExecutableElement)element;            System.out.println(executableElement.getReturnType());            System.out.println(executableElement.getSimpleName());            System.out.println(executableElement.getParameters());        }    }}

(旧的,失效的)apt 版本的处理器需要额外的方法来确定支持哪些注解以及支持的Java版本。不过,你现在可以简单的使用 @SupportedAnnotationTypes 和 @SupportedSourceVersion 注解。

你唯一需要实现的方法就是process(),这里是所有行为发生的地方。第一个参数告诉你哪个注解是存在的,第二个参数保留了剩余信息。通过使用process()的第二个参数,我们循环所有被@Simple 解的元素,并且针对每一个元素调用我们的display()方法。所有Element展示了自身的基本信息。例如,getModifiers()告诉你它是否为public和static。

Element只能执行那些编译器解析的所有基本对象共有的操作,而类和方法有额外的信息需要提取。所以需要检查它是哪种 ElementKind,然后将其向下转换为更具体的元素类型,转型成针对CLASS的TypeElement和针对METHOD的ExecutableElement,就可以为这些元素调用具体的方法了。

如果只是通过平常的方式来编译 SimpleTest.java,你不会得到任何结果。为了得到注解输出,你必须增加一个processor标志并且连接注解处理器类。首先编译Simple.java与SimpleProcessor.java,然后在annotations上层的目录执行:

javac -processor annotations.SimpleProcessor annotations/Simpl-eTest.java

现在就有了编译的输出:

annotations.Simple====== annotations.SimpleTest ======CLASS:[public]:SimpleTest:annotations.SimpleTestannotations.SimpleTestjava.lang.Objecti,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])====== i ======FIELD:[private]:i:int====== SimpleTest() ======CONSTRUCTOR:[public]::()void====== foo() ======METHOD:[public]:foo:()voidvoidfoo====== bar(java.lang.String,int,float) ======METHOD:[public]:bar:(java.lang.String,int,float)voidvoidbars,i,f====== main(java.lang.String[]) ======METHOD:[public, static]:main:(java.lang.String[])voidvoidmainargs

1.3.2. 更复杂的处理器

当你创建用于javac的注解处理器时,你不能使用Java的反射特性,因为你处理的是源代码,而并非是编译后的class文件。各种 mirror解决这个问题的方法是,通过允许你在未编译的源代码中查看方法、字段和类型。

如下是一个用于提取类中方法的注解,所以它可以被抽取成为一个接口:

package annotations;import java.lang.annotation.*;@Target(ElementType.TYPE)@Retention(RetentionPolicy.SOURCE)public @interface ExtractInterface {    String interfaceName() default "-!!-";}

接下来的测试类提供了一些公用方法,这些方法可以成为接口的一部分:

package annotations;@ExtractInterface(interfaceName = "IMultiplier")public class Multiplier {    public boolean flag = false;    private int n = 0;    public int multiplier(int x, int y) {        int total = 0;        for (int i = 0; i < x; i++) {            total = add(total, y);        }        return total;    }    public int fortySeven() {        return 47;    }    private int add(int x, int y) {        return x + y;    }    public double timesTen(double arg) {        return arg * 10;    }    public static void main(String[] args) {        Multiplier multiplier = new Multiplier();        System.out.println("11 * 16 = " + multiplier.multiplier(11, 16));    }}

这里有一个编译时处理器用于提取方法,并创建一个新的interface源代码文件,这个源文件将会在下一轮中被自动编译:

package annotations;import javax.annotation.processing.*;import javax.lang.model.SourceVersion;import javax.lang.model.element.*;import javax.lang.model.util.*;import java.util.*;import java.util.stream.*;import java.io.*;@SupportedAnnotationTypes("annotations.ExtractInterface")@SupportedSourceVersion(SourceVersion.RELEASE_8)public class IfaceExtractorProcessor extends AbstractProcessor {    private ArrayList            interfaceMethods = new ArrayList<>();    private Elements elementUtils;    private ProcessingEnvironment processingEnv;    @Override    public void init(            ProcessingEnvironment processingEnv) {        this.processingEnv = processingEnv;        elementUtils = processingEnv.getElementUtils();    }    @Override    public boolean process(            Set extends TypeElement> annotations,            RoundEnvironment env) {        for(Element elem:env.getElementsAnnotatedWith(                ExtractInterface.class)) {            String interfaceName = elem.getAnnotation(                    ExtractInterface.class).interfaceName();            for(Element enclosed :                    elem.getEnclosedElements()) {                if(enclosed.getKind()                        .equals(ElementKind.METHOD) &&                        enclosed.getModifiers()                                .contains(Modifier.PUBLIC) &&                        !enclosed.getModifiers()                                .contains(Modifier.STATIC)) {                    interfaceMethods.add(enclosed);                }            }            if(interfaceMethods.size() > 0)                writeInterfaceFile(interfaceName);        }        return false;    }    private void writeInterfaceFile(String interfaceName) {        try(Writer writer = processingEnv.getFiler()                        .createSourceFile(interfaceName)                        .openWriter())         {            String packageName = elementUtils                    .getPackageOf(interfaceMethods                            .get(0)).toString();            writer.write(                    "package " + packageName + ";\n");            writer.write("public interface " +                    interfaceName + " {\n");            for(Element elem : interfaceMethods) {                ExecutableElement method =                        (ExecutableElement)elem;                String signature = " public ";                signature += method.getReturnType() + " ";                signature += method.getSimpleName();                signature += createArgList(                        method.getParameters());                System.out.println(signature);                writer.write(signature + ";\n");            }            writer.write("}");        } catch(Exception e) {            throw new RuntimeException(e);        }    }    private String createArgList(            List extends VariableElement> parameters) {        String args = parameters.stream()                .map(p -> p.asType() + " " + p.getSimpleName())                .collect(Collectors.joining(", "));        return "(" + args + ")";    }}

Filer是PrintWriter的一种实例,可以用于创建新文件。我们使用Filer对象,而不是原生的PrintWriter原因是,这个对象可以运行javac追踪你创建的新文件,这使得它可以在新一轮中检查新文件中的注解并编译文件。

首先编译注解类与注解处理器类:javac IfaceExtractorProcessor.java ExtractInterface.java

然后在上级目录执行命令:javac -processor annotations.IfaceExtractorProcessor annotations/Multiplier.java

输出如下:

 public int multiplier (int x, int y) public int fortySeven () public double timesTen (double arg)

javac会在执行命令的目录生成的IMultiplier.java的文件,这个类同样会被javac编译(在某一轮中),所以你会在同一个目录中看到 IMultiplier.class 文件。如下所示:

package annotations;public interface IMultiplier { public int multiplier (int x, int y); public int fortySeven (); public double timesTen (double arg);}

1.4. 基于注解的单元测试

单元测试是对类中每个方法提供一个或者多个测试的一种事件,其目的是为了有规律的测试一个类中每个部分是否具备正确的行为。在 Java中,最著名的单元测试工具就是JUnit,在JUnit4版本中已经包含了注解。

通过注解,我们可以将单元测试集成在需要被测试的类中,从而将单元测试的时间和麻烦降到了最低。这种方式有额外的好处,就是使得测试私有方法和公有方法变的一样容易。

在这里我们将要自定义一个基于注解的测试框架叫做@Unit。我们用最开始定义的@Test来标记测试方法,测试方法不带参数,并返回boolean结果来说明测试方法成功或者失败,并且测试方法可以是private的。

1.4.1. 实现@Unit

首先我们需要定义所有的注解类型。这些都是简单的标签,并且没有任何字段。@Test标签在本章开头已经定义过了,这里是其他所需要的注解:

// TestObjectCleanup.javapackage atunit;import java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface TestObjectCleanup {}// TestObjectCreate.javapackage atunit;import java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface TestObjectCreate {}// TestProperty.javapackage atunit;import java.lang.annotation.*;@Target({ElementType.METHOD, ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface TestProperty {}

所有测试的保留属性都为 RUNTIME,这是因为 @Unit 必须在编译后的代码中发现这些注解。

要实现系统并运行测试,我们还需要反射机制来提取注解。下面这个程序通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。

package atunit;import java.io.File;import java.io.IOException;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.lang.reflect.Modifier;import java.nio.file.Files;import java.util.ArrayList;import java.util.List;public class AtUnit implements ProcessFiles.Strategy {    private static Class> testClass;    private static List failedTests = new ArrayList<>();    private static long testsRun = 0;    private static long failures = 0;    public static void main(String[] args) {        // 开启断言        ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);        // 执行测试        new ProcessFiles(new AtUnit(), "class").start(args);        if (failures == 0) {            System.out.println("OK(" + testsRun + "tests)");        } else {            System.out.println("(" + testsRun + "tests)");            System.out.println("\n>>>" + failures + " FAILURE" + (failures > 1 ? "S" : "") + "<<);            for (String failed : failedTests) {                System.out.println(" " + failed);            }        }    }    @Override    public void process(File file) {        try {            String cName = ClassNameFinder.thisClass(Files.readAllBytes(file.toPath()));            if (!cName.startsWith("public:")) return;            // 去掉访问权限修饰符            cName = cName.split(":")[1];            // 忽略不在包下的类            if (!cName.contains(".")) return;            testClass = Class.forName(cName);        } catch (IOException | ClassNotFoundException e) {            throw new RuntimeException(e);        }        TestMethods testMethods = new TestMethods();        Method creator = null;        Method cleanup = null;        for (Method method : testClass.getDeclaredMethods()) {            testMethods.addIfTestMethod(method);            if (creator == null) {                creator = checkForCreatorMethod(method);            }            if (cleanup == null) {                cleanup = checkForCleanupMethod(method);            }        }        if (testMethods.size() > 0) {            if (creator == null) {                try {                    if (!Modifier.isPublic(testClass.getDeclaredConstructor(null).getModifiers())) {                        System.out.println("Error: " + testClass + "no-arg constructor must be public");                        System.exit(1);                    }                } catch (NoSuchMethodException e) {                }            }            System.out.println(testClass.getName());        }        for (Method method : testMethods) {            System.out.print(" . " + method.getName() + " ");            try {                Object testObject = createTestObject(creator);                boolean success = false;                try {                    if (method.getReturnType().equals(boolean.class)) {                        success = (Boolean) method.invoke(testObject);                    } else {                        // 如果没有断言失败                        method.invoke(testObject);                        success = true;                    }                } catch (InvocationTargetException e) {                    System.out.println(e.getCause());                }                System.out.println(success ? "" : "(failed)");                testsRun++;                if (!success) {                    failures++;                    failedTests.add(testClass.getName() + ": " + method.getName());                }                if (cleanup != null) {                    cleanup.invoke(testObject, testObject);                }            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {                throw new RuntimeException(e);            }        }    }    public static class TestMethods extends ArrayList<Method> {        void addIfTestMethod(Method method) {            if (method.getAnnotation(Test.class) == null) return;            if (!(method.getReturnType().equals(boolean.class) | method.getReturnType().equals(void.class))) {                throw new RuntimeException("@Test method must return boolean or void");            }            // 将私有的方法设置为public            method.setAccessible(true);            add(method);        }    }    private static Method checkForCreatorMethod(Method method) {        if (method.getAnnotation(TestObjectCreate.class) == null) return null;        if (!method.getReturnType().equals(testClass))            throw new RuntimeException("@TestObjectCreate must return instance of Class to be tested");        if ((method.getModifiers() & Modifier.STATIC) < 1)            throw new RuntimeException("@TestObjectCreate must be static.");        method.setAccessible(true);        return method;    }    private static Method checkForCleanupMethod(Method method) {        if (method.getAnnotation(TestObjectCleanup.class) == null) return null;        if (!method.getReturnType().equals(void.class))            throw new RuntimeException("@TestObjectCleanup must return void");        if ((method.getModifiers() & Modifier.STATIC) < 1)            throw new RuntimeException("@TestObjectCleanup must be static.");        if (method.getParameterTypes().length == 0 || method.getParameterTypes()[0] != testClass)            throw new RuntimeException("@TestObjectCleanup must take an argument of the tested type.");        method.setAccessible(true);        return method;    }    private static Object createTestObject(Method creator) {        if (creator != null) {            try {                return creator.invoke(testClass);            } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {                throw new RuntimeException("Couldn't run TestObject (creator) method.");            }            // 使用无参构造器        } else {            try {                return testClass.newInstance();            } catch (InstantiationException | IllegalAccessException e) {                throw new RuntimeException("Couldn't create a test object. Try using a @TestObject method.");            }        }    }}

AtUnit.java使用了ProcessFiles工具进行逐步判断命令行中的参数,来决定它是一个目录还是文件,并采取相应的行为。这可以应用于不同的解决方法,是因为它包含了一个可用于自定义的Strategy接口:

package atunit;import java.io.File;import java.io.IOException;import java.nio.file.FileSystems;import java.nio.file.Files;import java.nio.file.PathMatcher;public class ProcessFiles {    public interface Strategy {        void process(File file);    }    private Strategy strategy;    private String ext;    public ProcessFiles(Strategy strategy, String ext) {        this.strategy = strategy;        this.ext = ext;    }    public void start(String[] args) {        try {            if (args.length == 0) {                processDirectoryTree(new File("."));            } else {                for (String arg : args) {                    File fileArg = new File(arg);                    if (fileArg.isDirectory()) {                        processDirectoryTree(fileArg);                    } else {                        if (!arg.endsWith("." + ext)) {                            arg += "." + ext;                        }                        strategy.process(new File(arg).getCanonicalFile());                    }                }            }        } catch (IOException e) {            throw new RuntimeException(e);        }    }    private void processDirectoryTree(File file) throws IOException {        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.{" + ext + "}");        Files.walk(file.toPath())                .filter(matcher::matches)                .forEach(p -> strategy.process(p.toFile()));    }    public static void main(String[] args) {        new ProcessFiles(file -> System.out.println(file), "java").start(args);    }}

AtUnit类实现了ProcessFiles.Strategy,其包含了一个process()方法。在这种方式下,AtUnit实例可以作为参数传递给 ProcessFiles构造器。第二个构造器的参数告诉ProcessFiles寻找所有包含特定拓展名的文件,这里用的是“.class”。

与@JUnit相比,因为@Unit会自动找到可测试的类和方法,所以不需要“套件”(suites)机制。而在@JUnit中,必须告诉测试工具你打算测试什么,这就要求用套件来组织测试,以便JUnit能够找到它们,并运行其中包含的测试。

AtUnit.java中存在的一个我们必须要解决的问题是,当它发现类文件时,类文件名中的限定类名(包括包)不明显。为了发现这个信息,必须解析类文件。找到.class文件时,会打开它并读取其二进制数据并将其传递给ClassNameFinder.thisClass()。在这里,我们会涉及到字节码相关内容:

package atunit;import java.io.ByteArrayInputStream;import java.io.DataInputStream;import java.io.IOException;import java.nio.file.FileSystems;import java.nio.file.Files;import java.nio.file.PathMatcher;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class ClassNameFinder {    public static String thisClass(byte[] classBytes) {        Map offsetTable = new HashMap<>();        Map classNameTable = new HashMap<>();        try {            DataInputStream data = new DataInputStream(new ByteArrayInputStream(classBytes));            // 字节码开头的魔术值:0xcafebabe            int magic = data.readInt();            // 次版本号            int minorVersion = data.readShort();            // 主版本号            int majorVersion = data.readShort();            // 常量池大小            int constantPoolCount = data.readShort();            int[] constantPool = new int[constantPoolCount];            for (int i = 1; i < constantPoolCount; i++) {                int tag = data.read();                switch (tag) {                    case 1: // UTF                        // 获取到className                        int length = data.readShort();                        char[] bytes = new char[length];                        for (int k = 0; k < length; k++) {                            bytes[k] = (char) data.read();                        }                        String className = new String(bytes);                        classNameTable.put(i, className);                        break;                    case 5: // LONG                    case 6: // DOUBLE                        // 丢掉8个字节,进行跳过                        data.readLong();                        i++;                        break;                    case 7: // CLASS                        // 找到类的坐标                        int offset = data.readShort();                        offsetTable.put(i, offset);                        break;                    case 8: // STRING                        // 丢掉两个字节                        data.readShort();                        break;                    case 3: // INTEGER                    case 4: // FLOAT                    case 9: // FIELD_REF                    case 10: // METHOD_REF                    case 11: // INTERFACE_METHOD_REF                    case 12: // NAME_AND_TYPE                    case 18: // Invoke Dynamic                        // 丢掉四个字节                        data.readInt();                        break;                    case 15: // Method Handle                        data.readByte();                        data.readShort();                        break;                    case 16: // Method Type                        data.readShort();                        break;                    default:                        throw new RuntimeException("Bad tag " + tag);                }            }            short accessFlags = data.readShort();            String access = (accessFlags & 0x0001) == 0 ? "nonpublic:" : "public:";            int thisClass = data.readShort();            int superClass = data.readShort();            return access + classNameTable.get(offsetTable.get(thisClass)).replace('/', '.');        } catch (IOException | RuntimeException e) {            throw new RuntimeException(e);        }    }    public static void main(String[] args)throws Exception {        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.class");        Files.walk(Paths.get("."))                .filter(matcher::matches)                .map(p -> {                    try {                        return thisClass(Files.readAllBytes(p));                    } catch (Exception e) {                        throw new RuntimeException(e);                    }                }).filter(s -> s.startsWith("public:"))                .map(s -> s.split(":")[1])                .filter(s -> !s.startsWith("enums."))                .filter(s -> s.contains("."))                .forEach(System.out::println);    }}

在字节码内容中的常量池里,每一个元素,其长度可能是固定式也可能是可变的值,因此我们必须检查每一个常量的起始标记,然后才能知道该怎么做,这就是 switch 语句的工作。

因为我们并不打算精确的分析类中所有的数据,所以仅仅是从文件的起始一步一步的走,直到取得我们所需的信息,因此你会发现,在这个过程中我们丢弃了大量的数据。关于类的信息都保存在classNameTable和offsetTable中。在读取常量池之后,就找到了this_class信息,这是offsetTable的一个坐标,通过它可以找到进入classNameTable的坐标,然后就可以得到我们所需的类的名字了。

1.4.2. 使用@Unit

下面是一个简单的例子:

package annotations;import atunit.Test;public class AtUnitExample1 {    public String methodOne() {        return "This is methodOne";    }    public int methodTwo() {        System.out.print("This is methodTwo");        return 2;    }    @Test    boolean methodOneTest() {        return methodOne().endsWith("This is methodOne");    }    @Test    boolean m2() {        return methodTwo() == 2;    }    @Test    private boolean m3() {        return true;    }    // 展示失败的输出    @Test    boolean failureTest() {        return false;    }    @Test    boolean anotherDisappointment() {        return false;    }}

执行下面的命令,会得到输出:

shell> javac atunit/*.javashell> javac annotations/*.javashell> java atunit.AtUnit annotations/AtUnitExample1.classUnitExample1.classannotations.AtUnitExample1 . methodOneTest . m2 This is methodTwo . m3 . failureTest (failed) . anotherDisappointment (failed)(5tests)>>>2 FAILURES<<< annotations.AtUnitExample1: failureTest annotations.AtUnitExample1: anotherDisappointment

使用 @Unit 进行测试的类必须定义在某个包中。

你并非必须将测试方法嵌入到原来的类中,有时候这种事情根本做不到。要生产一个非嵌入式的测试,最简单的方式就是继承:

package annotations;import atunit.Test;public class AUExternalTest extends AtUnitExample1 {    @Test    boolean _MethodOne() {        return methodOne().equals("This is methodOne");    }    @Test    boolean _MethodTwo() {        return methodTwo() == 2;    }}

执行下面的命令,会得到输出:

shell>  javac annotations/AUExternalTest.javashell>  java atunit.AtUnit annotations/AUExternalTest.classannotations.AUExternalTest . _MethodOne . _MethodTwo This is methodTwoOK(2tests)

也可以使用组合来创建非嵌入式的测试:

package annotations;import atunit.Test;public class AuComposition {    AtUnitExample1 testObject = new AtUnitExample1();    @Test    boolean tMethodOne() {        return testObject.methodOne().equals("This is methodOne");    }    @Test    boolean tMethodTwo(){        return testObject.methodTwo() == 2;    }}

执行下面的命令,会得到输出:

shell>  javac annotations/AuComposition.javashell>  java atunit.AtUnit annotations/AuComposition.classannotations.AuComposition . tMethodOne . tMethodTwo This is methodTwoOK(2tests)

为了表示测试成功,可以使用Java的assert语句。想使用Java的断言机制需要在java命令行中加上-ea标志来开启,不过@Unit 已经自动开启了该功能。一个失败的assert或者从方法抛出的异常都被视为测试失败,但是@Unit不会在这个失败的测试上卡住,它会继续运行,直到所有测试完毕,下面是一个示例程序:

package annotations;import atunit.Test;import java.io.FileInputStream;import java.io.IOException;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 in = new FileInputStream("nofile.txt")        ) {        }    }    @Test    boolean assertAndReturn() {        assert methodTwo() == 2 : "methodTwo must equal 2";        return methodOne().equals("This is methodOne");    }}

执行下面的命令,会得到输出:

shell> javac annotations/AtUnitExample2.javashell> java atunit.AtUnit annotations/AtUnitExample2.classannotations.AtUnitExample2 . assertExample . assertFailureExample java.lang.AssertionError: What a surprise!(failed) . exceptionExample java.io.FileNotFoundException: nofile.txt (No such file or directory)(failed) . assertAndReturn This is methodTwo(4tests)>>>2 FAILURES<<< annotations.AtUnitExample2: assertFailureExample annotations.AtUnitExample2: exceptionExample

下是一个使用非嵌入式测试的例子,并且使用了断言,它将会对java.util.HashSet进行一些简单的测试:

package annotations;import atunit.Test;import java.util.HashSet;public class HashSetTest {    HashSet 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注解标记该方法。@TestObjectCreate修饰的方法必须声明为static,并且必须返回一个你正在测试的类型对象,这一切都由@Unit负责确保成立。例子如下:

package annotations;import atunit.Test;import atunit.TestObjectCreate;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.print("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;    }}

执行下面的命令,会得到输出:

shell> javac annotations/AtUnitExample3.javashell> java atunit.AtUnit annotations/AtUnitExample3.classUnitExample3.classannotations.AtUnitExample3 . initialization  . methodOneTest  . m2 This is methodTwoOK(3tests)

有的时候,你需要向单元测试中增加一些字段。这时候可以使用@TestProperty注解,由它注解的字段表示只在单元测试中使用(因此,在你将产品发布给客户之前,他们应该被删除)。@TestProperty也可以用来标记那些只在测试中使用的方法,但是它们本身不是测试方法。在下面的例子中,一个String通过String.split()方法进行分割,从其中读取一个值,这个值将会被生成测试对象:

package annotations;import atunit.Test;import atunit.TestObjectCreate;import atunit.TestProperty;import java.util.*;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 random = new Random();    public AtUnitExample4(String word) {        this.word = word;    }    public String getWord() {        return word;    }    public String scrambleWorld() {        List chars = new ArrayList<>();        for(Character c: word.toCharArray()){            chars.add(c);        }        Collections.shuffle(chars, random);        StringBuilder result = new StringBuilder();        for (char ch : chars) {            result.append(ch);        }        return result.toString();    }    @TestProperty    static List input = Arrays.asList(theory.split(" "));    @TestProperty    static Iterator words = input.iterator();    @TestObjectCreate    static AtUnitExample4 create(){        if(words.hasNext()){            return new AtUnitExample4(words.next());        }        return null;    }    @Test    boolean words(){        System.out.println("'" + getWord() + "'");        return getWord().equals("are");    }    @Test    boolean scramble1(){        random = new Random(47);        System.out.println("'" + getWord() + "'");        String scrambled = scrambleWorld();        System.out.println(scrambled);        return scrambled.equals("1A1");    }    @Test    boolean scramble2(){        random = new Random(74);        System.out.println("'" + getWord() + "'");        String scrambled = scrambleWorld();        System.out.println(scrambled);        return scrambled.equals("tsaeborornussu");    }}

执行下面的命令,会得到输出:

shell> javac annotations/AtUnitExample4.javashell> java atunit.AtUnit annotations/AtUnitExample4.classannotations.AtUnitExample4 . words 'All'(failed) . scramble1 'brontosauruses'ntsaueorosurbs(failed) . scramble2 'are'are(failed)(3tests)>>>3 FAILURES<<< annotations.AtUnitExample4: words annotations.AtUnitExample4: scramble1 annotations.AtUnitExample4: scramble2

如果你的测试对象需要执行某些初始化工作,并且使用完成之后还需要执行清理工作,那么可以选择使用static的 @TestObjectCleanup方法,当测试对象使用结束之后,该方法会为你执行清理工作。在下面的示例中,@TestObjectCleanup为每一个测试对象都打开了一个文件,因此必须在丢弃测试的时候关闭该文件:

package annotations;import atunit.Test;import atunit.TestObjectCleanup;import atunit.TestObjectCreate;import atunit.TestProperty;import java.io.IOException;import java.io.PrintWriter;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;    }}

执行下面的命令,会得到输出:

shell> javac annotations/AtUnitExample5.javashell> java atunit.AtUnit annotations/AtUnitExample5.classannotations.AtUnitExample5annotations.AtUnitExample5 . test1 Running cleanup . test2 Running cleanup . test3 Running cleanupOK(3tests)

1.4.3. 在@Unit中使用泛型

泛型为 @Unit 出了一个难题,因为我们不可能“通用测试”。我们必须针对某个特定类型的参数或者参数集才能进行测试。解决方法十分简单,让测试类继承自泛型类的一个特定版本即可。下面是一个 stack 的简单实现:

package annotations;import java.util.LinkedList;public class StackL<T> {    private LinkedList list = new LinkedList<>();    public void push(T v) {        list.addFirst(v);    }    public T top(){        return list.getFirst();    }    public T pop(){        return list.removeFirst();    }}

为了测试 String 版本,我们直接让测试类继承一个Stack:

package annotations;import atunit.Test;public class StackLStringTst extends StackL<String> {    @Test    void tPush() {        push("one");        assert top().equals("one");        push("two");        assert top().equals("two");    }    @Test    void tPop() {        push("one");        push("two");        assert pop().equals("two");        assert pop().equals("one");    }    @Test    void tTop() {        push("A");        push("B");        assert top().equals("B");        assert top().equals("B");    }}

这种方法存在的唯一缺点是,继承使我们失去了访问被测试的类中private方法的能力。如果你需要测试private方法,那你要么把private方法变为protected,要么添加一个非private的@TestProperty方法,由它来调用private方法

下面是输出:

shell> javac annotations/StackLStringTst.javashell> java atunit.AtUnit annotations/StackLStringTst.classannotations.StackLStringTst . tPush  . tTop  . tPop OK(3tests)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值