ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
### 实现 @Unit 首先我们需要定义所有的注解类型。这些都是简单的标签,并且没有任何字段。@Test 标签在本章开头已经定义过了,这里是其他所需要的注解: ```java // onjava/atunit/TestObjectCreate.java // The @Unit @TestObjectCreate tag package onjava.atunit; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TestObjectCreate {} ``` ```java // onjava/atunit/TestObjectCleanup.java // The @Unit @TestObjectCleanup tag package onjava.atunit; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TestObjectCleanup {} ``` ```java // onjava/atunit/TestProperty.java // The @Unit @TestProperty tag package onjava.atunit; import java.lang.annotation.*; // Both fields and methods can be tagged as properties: @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TestProperty {} ``` 所有测试的保留属性都为 **RUNTIME**,这是因为 **@Unit** 必须在编译后的代码中发现这些注解。 要实现系统并运行测试,我们还需要反射机制来提取注解。下面这个程序通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。正是由于注解帮助,这个程序才会如此短小而直接: ```java // onjava/atunit/AtUnit.java // An annotation-based unit-test framework // {java onjava.atunit.AtUnit} package onjava.atunit; import java.lang.reflect.*; import java.io.*; import java.util.*; import java.nio.file.*; import java.util.stream.*; import onjava.*; public class AtUnit implements ProcessFiles.Strategy { static Class<?> testClass; static List<String> failedTests= new ArrayList<>(); static long testsRun = 0; static long failures = 0; public static void main(String[] args) throws Exception { ClassLoader.getSystemClassLoader() .setDefaultAssertionStatus(true); // Enable assert 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 cFile) { try { String cName = ClassNameFinder.thisClass( Files.readAllBytes(cFile.toPath())); if(!cName.startsWith("public:")) return; cName = cName.split(":")[1]; if(!cName.contains(".")) return; // Ignore unpackaged classes testClass = Class.forName(cName); } catch(IOException | ClassNotFoundException e) { throw new RuntimeException(e); } TestMethods testMethods = new TestMethods(); Method creator = null; Method cleanup = null; for(Method m : testClass.getDeclaredMethods()) { testMethods.addIfTestMethod(m); if(creator == null) creator = checkForCreatorMethod(m); if(cleanup == null) cleanup = checkForCleanupMethod(m); } if(testMethods.size() > 0) { if(creator == null) try { if(!Modifier.isPublic(testClass .getDeclaredConstructor() .getModifiers())) { System.out.println("Error: " + testClass + " no-arg constructor must be public"); System.exit(1); } } catch(NoSuchMethodException e) { // Synthesized no-arg constructor; OK } System.out.println(testClass.getName()); } for(Method m : testMethods) { System.out.print(" . " + m.getName() + " "); try { Object testObject = createTestObject(creator); boolean success = false; try { if(m.getReturnType().equals(boolean.class)) success = (Boolean)m.invoke(testObject); else { m.invoke(testObject); success = true; // If no assert fails } } catch(InvocationTargetException e) { // Actual exception is inside e: System.out.println(e.getCause()); } System.out.println(success ? "" : "(failed)"); testsRun++; if(!success) { failures++; failedTests.add(testClass.getName() + ": " + m.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 m) { if(m.getAnnotation(Test.class) == null) return; if(!(m.getReturnType().equals(boolean.class) || m.getReturnType().equals(void.class))) throw new RuntimeException("@Test method" + " must return boolean or void"); m.setAccessible(true); // If it's private, etc. add(m); } } private static Method checkForCreatorMethod(Method m) { if(m.getAnnotation(TestObjectCreate.class) == null) return null; if(!m.getReturnType().equals(testClass)) throw new RuntimeException("@TestObjectCreate " + "must return instance of Class to be tested"); if((m.getModifiers() & java.lang.reflect.Modifier.STATIC) < 1) throw new RuntimeException("@TestObjectCreate " + "must be static."); m.setAccessible(true); return m; } private static Method checkForCleanupMethod(Method m) { if(m.getAnnotation(TestObjectCleanup.class) == null) return null; if(!m.getReturnType().equals(void.class)) throw new RuntimeException("@TestObjectCleanup " + "must return void"); if((m.getModifiers() & java.lang.reflect.Modifier.STATIC) < 1) throw new RuntimeException("@TestObjectCleanup " + "must be static."); if(m.getParameterTypes().length == 0 || m.getParameterTypes()[0] != testClass) throw new RuntimeException("@TestObjectCleanup " + "must take an argument of the tested type."); m.setAccessible(true); return m; } private static Object createTestObject(Method creator) { if(creator != null) { try { return creator.invoke(testClass); } catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException("Couldn't run " + "@TestObject (creator) method."); } } else { // Use the no-arg constructor: 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** 接口: ```java // onjava/ProcessFiles.java package onjava; import java.io.*; import java.nio.file.*; 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 { // Allow user to leave off extension: if(!arg.endsWith("." + ext)) arg += "." + ext; strategy.process( new File(arg).getCanonicalFile()); } } } catch(IOException e) { throw new RuntimeException(e); } } public void processDirectoryTree(File root) throws IOException { PathMatcher matcher = FileSystems.getDefault() .getPathMatcher("glob:**/*.{" + ext + "}"); Files.walk(root.toPath()) .filter(matcher::matches) .forEach(p -> strategy.process(p.toFile())); } } ``` **AtUnit** 类实现了 **ProcessFiles.Strategy**,其包含了一个 `process()` 方法。在这种方式下,**AtUnit** 实例可以作为参数传递给 **ProcessFiles** 构造器。第二个构造器的参数告诉 **ProcessFiles** 如寻找所有包含 “class” 拓展名的文件。 如下是一个简单的使用示例: ```java // annotations/DemoProcessFiles.java import onjava.ProcessFiles; public class DemoProcessFiles { public static void main(String[] args) { new ProcessFiles(file -> System.out.println(file), "java").start(args); } } ``` 输出为: ```java .\AtUnitExample1.java .\AtUnitExample2.java .\AtUnitExample3.java .\AtUnitExample4.java .\AtUnitExample5.java .\AUComposition.java .\AUExternalTest.java .\database\Constraints.java .\database\DBTable.java .\database\Member.java .\database\SQLInteger.java .\database\SQLString.java .\database\TableCreator.java .\database\Uniqueness.java .\DemoProcessFiles.java .\HashSetTest.java .\ifx\ExtractInterface.java .\ifx\IfaceExtractorProcessor.java .\ifx\Multiplier.java .\PasswordUtils.java .\simplest\Simple.java .\simplest\SimpleProcessor.java .\simplest\SimpleTest.java .\SimulatingNull.java .\StackL.java .\StackLStringTst.java .\Testable.java .\UseCase.java .\UseCaseTracker.java ``` 如果没有命令行参数,这个程序会遍历当前的目录树。你还可以提供多个参数,这些参数可以是类文件(带或不带.class扩展名)或目录。 回到我们对 **AtUnit.java** 的讨论,因为 **@Unit** 会自动找到可测试的类和方法,所以不需要“套件”机制。 **AtUnit.java** 中存在的一个我们必须要解决的问题是,当它发现类文件时,类文件名中的限定类名(包括包)不明显。为了发现这个信息,必须解析类文件 - 这不是微不足道的,但也不是不可能的。 找到 .class 文件时,会打开它并读取其二进制数据并将其传递给 `ClassNameFinder.thisClass()`。 在这里,我们正在进入“字节码工程”领域,因为我们实际上正在分析类文件的内容: ```java // onjava/atunit/ClassNameFinder.java // {java onjava.atunit.ClassNameFinder} package onjava.atunit; import java.io.*; import java.nio.file.*; import java.util.*; import onjava.*; public class ClassNameFinder { public static String thisClass(byte[] classBytes) { Map<Integer,Integer> offsetTable = new HashMap<>(); Map<Integer,String> classNameTable = new HashMap<>(); try { DataInputStream data = new DataInputStream( new ByteArrayInputStream(classBytes)); int magic = data.readInt(); // 0xcafebabe 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(); // int tableSize; switch(tag) { case 1: // UTF int length = data.readShort(); char[] bytes = new char[length]; for(int k = 0; k < bytes.length; k++) bytes[k] = (char)data.read(); String className = new String(bytes); classNameTable.put(i, className); break; case 5: // LONG case 6: // DOUBLE data.readLong(); // discard 8 bytes i++; // Special skip necessary break; case 7: // CLASS int offset = data.readShort(); offsetTable.put(i, offset); break; case 8: // STRING data.readShort(); // discard 2 bytes 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(); // discard 4 bytes 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); } } // Demonstration: public static void main(String[] args) throws Exception { PathMatcher matcher = FileSystems.getDefault() .getPathMatcher("glob:**/*.class"); // Walk the entire tree: 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:")) // .filter(s -> s.indexOf('$') >= 0) .map(s -> s.split(":")[1]) .filter(s -> !s.startsWith("enums.")) .filter(s -> s.contains(".")) .forEach(System.out::println); } } ``` 输出为: ```java onjava.ArrayShow onjava.atunit.AtUnit$TestMethods onjava.atunit.AtUnit onjava.atunit.ClassNameFinder onjava.atunit.Test onjava.atunit.TestObjectCleanup onjava.atunit.TestObjectCreate onjava.atunit.TestProperty onjava.BasicSupplier onjava.CollectionMethodDifferences onjava.ConvertTo onjava.Count$Boolean onjava.Count$Byte onjava.Count$Character onjava.Count$Double onjava.Count$Float onjava.Count$Integer onjava.Count$Long onjava.Count$Pboolean onjava.Count$Pbyte onjava.Count$Pchar onjava.Count$Pdouble onjava.Count$Pfloat onjava.Count$Pint onjava.Count$Plong onjava.Count$Pshort onjava.Count$Short onjava.Count onjava.CountingIntegerList onjava.CountMap onjava.Countries onjava.Enums onjava.FillMap onjava.HTMLColors onjava.MouseClick onjava.Nap onjava.Null onjava.Operations onjava.OSExecute onjava.OSExecuteException onjava.Pair onjava.ProcessFiles$Strategy onjava.ProcessFiles onjava.Rand$Boolean onjava.Rand$Byte onjava.Rand$Character onjava.Rand$Double onjava.Rand$Float onjava.Rand$Integer onjava.Rand$Long onjava.Rand$Pboolean onjava.Rand$Pbyte onjava.Rand$Pchar onjava.Rand$Pdouble onjava.Rand$Pfloat onjava.Rand$Pint onjava.Rand$Plong onjava.Rand$Pshort onjava.Rand$Short onjava.Rand$String onjava.Rand onjava.Range onjava.Repeat onjava.RmDir onjava.Sets onjava.Stack onjava.Suppliers onjava.TimedAbort onjava.Timer onjava.Tuple onjava.Tuple2 onjava.Tuple3 onjava.Tuple4 onjava.Tuple5 onjava.TypeCounter ``` 虽然无法在这里介绍其中所有的细节,但是每个类文件都必须遵循一定的格式,而我已经尽力用有意义的字段来表示这些从 **ByteArrayInputStream** 中提取出来的数据片段。通过施加在输入流上的读操作,你能看出每个信息片的大小。例如每一个类的头 32 个 bit 总是一个 “神秘数字” **0xcafebabe**,而接下来的两个 **short** 值是版本信息。常量池包含了程序的常量,所以这是一个可变的值。接下来的 **short** 告诉我们这个常量池有多大,然后我们为其创建一个尺寸合适的数组。常量池中的每一个元素,其长度可能是固定式,也可能是可变的值,因此我们必须检查每一个常量的起始标记,然后才能知道该怎么做,这就是 switch 语句的工作。我们并不打算精确的分析类中所有的数据,仅仅是从文件的起始一步一步的走,直到取得我们所需的信息,因此你会发现,在这个过程中我们丢弃了大量的数据。关于类的信息都保存在 **classNameTable** 和 **offsetTable** 中。在读取常量池之后,就找到了 **this_class** 信息,这是 **offsetTable** 的一个坐标,通过它可以找到进入 **classNameTable** 的坐标,然后就可以得到我们所需的类的名字了。 现在让我们回到 **AtUtil.java** 中,process() 方法中拥有了类的名字,然后检查它是否包含“.”,如果有就表示该类定义于一个包中。没有包的类会被忽略。如果一个类在包中,那么我们就可以使用标准的类加载器通过 `Class.forName()` 将其加载进来。现在我们可以对这个类进行 **@Unit** 注解的分析工作了。 我们只需要关注三件事:首先是 **@Test** 方法,它们被保存在 **TestMehtods** 列表中,然后检查其是否具有 @TestObjectCreate 和 **@TestObjectCleanup****** 方法。从代码中可以看到,我们通过调用相应的方法来查询注解从而找到这些方法。 每找到一个 @Test 方法,就打印出来当前类的名字,于是观察者立刻就可以知道发生了什么。接下来开始执行测试,也就是打印出方法名,然后调用 createTestObject() (如果存在一个加了 @TestObjectCreate 注解的方法),或者调用默认构造器。一旦创建出来测试对象,如果调用其上的测试方法。如果测试的返回值为 boolean,就捕获该结果。如果测试方法没有返回值,那么就没有异常发生,我们就假设测试成功,反之,如果当 assert 失败或者有任何异常抛出的时候,就说明测试失败,这时将异常信息打印出来以显示错误的原因。如果有失败的测试发生,那么还要统计失败的次数,并将失败所属的类和方法加入到 failedTests 中,以便最后报告给用户。