| Introducing Testinium (v 1.0) | ||
| What
is Testinium? Testinium is a pet-project of mine. Its goal was to be used as a replacement of JUnit for J2SE 5.0 applications. I never published the source code, until now. Once I became aware of TestNG, I stopped using my version because I had not developed an Eclipse plug-in (although planned), and TestNG provided a plugin. Testinium was never meant to be as complete as what TestNG provides, or what JUnit 4 will provide, but let's see what I can do with my free time. I have decided to walk you through some stuff I have done for Testinium. Consider this as a tutorial of writing your own Testing Framework...(YATF). Thierry Janaudy License > Testinium is released under Apache License Version 2.0 |
||
| How do I write a
Unit Testing framework? The choice is yours, but when I started to work on Testinium, I did not have to worry about JUnit, legacy-code or any other things. I just focused on J2SE 5.0. NUnit and TestNG introduced the concepts of attributes (or annotations) for testing purposes. TestNG uses the @Test annotation to annotate methods that need to be tested, with some other stuff like number of methods invocations, groups and dependencies,... Testinium defines a @Testable annotation. |
||
| Dependencies One of the great features of TestNG is the notion of groups and dependencies between groups and methods. When re-building Testinium, I decided to keep that feature using the @Group annotation. Testinium has the following characteristics: > A @Testable method with no @Group belongs to "Default Group" > A @Testable method can depend on zero or more @Testable methods > A @Group can depend on zero or more @Group groups |
||
| Annotations With the previous elements in mind, we can start to design the annotations : |
||
|
||
| Testable.java | ||
|
||
| Group.java | ||
| A basic use of those annotations would be : | ||
|
||
| BasicTest.java | ||
| Sorting out dependencies The next step in writing a Unit Testing framework is to work out the dependencies, what method should be invoked first, and detect cycles if any. You can use a mix of Depth Search First algorithm with some Topological sorting. The DFS algo is : |
||
|
||
| DFS Algorithm | ||
| The previous DFS algorithm can be enhanced to detect cycles : | ||
|
||
| DFS Algorithm with Cycle Detection | ||
| Once nodes have been VISITED, they are added to a list, forming a topologically sorted list. | ||
| Working with classes
and instances If Testinium is given a .class, it can check for annotations using the method getAnnotations() from java.lang.Class. However, you may want to be a little bit more flexible than this. Because there is no notion of annotation inheritance on the Java platform, what if you annotate a Remote interface (like an EJB interface) using @Testable? Nothing if you just give the instance (because you might get a proxy, or an inherited class), and nothing if you just give the .class. Testinium supports the following : .class > Testinium uses newInstance() to get the instance (hence you must provide a zero-argument constructor) instance > Testinium uses getClass() to get the Class object .class and instance > Testinium uses the .class to get the annotations and the instance to invoke the @Testable methods ClassInfo.java is the class holding that information : |
||
|
||
| ClassInfo.java | ||
| Have you seen an
annotation somewhere? The next step is to look for annotations in the classes provided to Testinium. The @Testable/@Group methods are placed into a graph. A graph has nodes, a node may have children and/or parents. Each node has a containee, the node being the container. For each class that Testinium processes, we have to go through all the annotated methods and build the graph. > If a method is annotated with @Group only, a RuntimeException is thrown > If a method is annotated with @Testable only, it is placed in a group called "Default Group" > If a method has dependencies on other methods, this method becomes the parent node of all the other dependent methods > If a group has dependencies on other groups, this method becomes the parent node of all the other dependent methods |
||
![]() |
||
| Processing the classes The next big thing is to process the classes and build the non-yet-sorted graph. This is done by the process() method in ClassInfo.java (Could be optimized - will be refactored in new versions of Testinium) : |
||
|
||
| process() | ||
Calling DFS on BasicTest.javaThe following list is the topologically sorted nodes list for BasicTest.java :
[Testinium] version 1.0
Node Default Group
-> MethodInfo: m1
-------------------------
Node GroupA
<- GroupB <- GroupC
-> MethodInfo: m2
-------------------------
Node MethodInfo: m1
<- Default Group <- MethodInfo: m2
-------------------------
Node MethodInfo: m3
<- GroupB <- GroupC
-------------------------
Node GroupB
-> MethodInfo: m3 -> GroupA
-------------------------
Node GroupC
-> MethodInfo: m3 -> GroupA
-------------------------
Node MethodInfo: m2
<- GroupA
-> MethodInfo: m1
-------------------------
MethodInfo: m1 <- Default Group <- MethodInfo: m2 <- GroupA <- MethodInfo: m3 <- GroupB <- GroupC
|
||
![]() |
||
| You should read the last
bit <- as "dependent on" : > GroupC depends on GroupB > GroupB depends on m3 > m3 depends on GroupA > GroupA depends on m2 > m2 depends on Default Group > Default Group depends on m1 And the methods will be invoked in the following order : > m1, m2, m3 |
||
| Checking on a failed
test Testinium supports basic exceptions and the assert keyword from J2SE 5.0. To mark a test as a failed test, you must use the assert keyword or throw an exception. If throwing an exception is part of the expected behaviour of the test method, it should be declared using the @ExpectableExceptions annotation |
||
|
||
| ExpectableExceptions.java | ||
| Invoking the test methods Methods are called by the invokeTests() method in ClassInfo.java: |
||
|
||
| invokeTests() | ||
|
That's it! We have now a basic testing framework on top of which we can build. Note that this is not
intended for production. Let's see how we can extend Testinium to support other features. |
||
| Generating reports In order to receive events on tested methods, implement the MethodInvokerListener interface: |
||
|
||
| MethodInvokerListener.java | ||
|
.. and register it via: |
||
|
||
| testinium.addListener | ||
|
I may add this in a simple config file. But I won't bother because you'll get the source code to DYI.
Testinium is provided with two listeners: a ConsoleReporter and a HTMLReporter |
||
| Number of invocations TestNG introduced the invocationCount attribute. It is a very neat way to tell the testing engine to invoke the test method more than once. You may use this to measure performance on a method call. Let's add an attribute numberOfInvocations to the @Testable annotation: |
||
|
||
| Amended Testable.java | ||
|
If you choose numberOfInvocations > 1, what should happen to the time measured? If one test fails but
nine tests succeed, do you mark this test as failed? TestNG solves this by using a successPercentage()
attribute. I will add support for this in version 2.0. For now, Testinium 1.0 will mark the test as a failed
test if there is at least one failure. The time is the average invocation time.
The result is shown in this HTML Report. |
||
| Parallel testing and timeouts The most enjoyable feature of TestNG is its support for parallel testing and timeout. We need some thinking here to add that feature to Testinium. The Concurrency Utilities provide everything we need to add support for parallel testing and timeouts to Testinium. In order to implement parallel testing, we can use ThreadPoolExecutor that is part of the Concurrency Utilities of J2SE 5.0. The idea is to have a pool of threads (with one active thread by default) that execute FutureTasks. The number of threads would need to be configured at the class level (We need another annotation at the class level). Each FutureTask has the public V get(long timeout, TimeUnit unit) method that can be used to figure out if a timeout has expired before the method has completed. But first, let's define a @Configuration annotation that could be used at the class level to define the parallel strategy. |
||
| Configuring the parallel mode |
||
|
||
| Configuration.java | ||
|
A sample class would look like this: |
||
|
||
| ParallelTest.java | ||
|
Note that if parallel is set to false, it does not matter how many threads you have,
Testinium will use one (1) thread.
We also need to amend the @Testable annotation with a timeout attribute: |
||
|
||
| Amended Testable.java | ||
| Result With all of this in place, let's test the following class: |
||
|
||
| ParallelTest.java | ||
|
Testinium generates the following HTML report. |
||
| What's next? It took me about 2 days to write Testinium 1.0. Most of the ideas come from TestNG, so if you want a piece of advice, use TestNG. I would like to work on Testinium 2.0, a re-write and refactored version with the following features: > A plug-in architecture to allow your own sorting algorithm for example. You would be given a graph, and you would return a sorted list of methods to invoke, and so on > Refactored code, because j'ai développé ça comme un cochon > Develop an Eclipse plug-in > Develop a NetBeans plug-in > Add TestNG tests to test Testinium :-) > Add Testinium tests to test Testinium and compare them with TestNG > Fix the numerous bugs that I have not yet found (I am not pleased with my use of ThreadPoolExecutor) > An ANT task Also, Testinium will always target J2SE 5.0 and beyond. Testinium will never support JUnit. |
||