Gerd has posted 5 posts at DZone. View Full User Profile

Defining & Testing Java Application Architectures with ztest

07.12.2008
| 5847 views |
  • submit to reddit
One of the important aspects of software systems is architecture.

Every software application has an inner structure, defined by the classes it contains and the relations between them. An architecture puts constraints on these relations. It can do so by specifying sets of classes and the allowed or forbidden relations between them.

Sets of classes include all classes that share a common property, like being located in a package, implementing a interface, being annotated with some annotation, obeying the same name convention.

Classes are related to each other. Examples of relations are: has a field of type, uses in method signature, uses in code, extends, implements.

An application has a defined architecture if it is possible to identify meaningful class sets and consistent dependencies between them. Doing so without tool support is hard, due to the dynamic nature and complexity of software systems.

ztest

ztest is an open source framework that supports the specification of application architectures, as stated above. ztest can be used in junit tests to enforce the architecture. Once defined, the architecture can be reused in other applications too.

Imagine starting a new project, with a new framework. You'll be busy learning the framework and until you get used to it and slowly learn the best way to use it you will surely make all the mistakes others did.

Now imagine the framework comes with one or many predefined architectures, based on the experience gained by others. Look at the architecture description, code, and then verify that you have applied it correctly by running a junit test.

If you already used junit tests, you may have experienced the effect that just being forced to write a test you have to rethink your interfaces. The same thing happens when writing a ztest. You have to think about the architecture and to define the dependencies. This alone leads to a better architecture and code structure.

How it works

ztest scans a directory or a archive for class-files. It extracts information from the class files and builds a repository of class meta information. The information is organized as a dependency graph with classes as nodes and edges between classes that are somehow related. The information can be queried with filters. There are 2 kinds of filters: class filters and edge filters. Class filters are applied to the nodes of the dependency graph and edge filters to the edges. There is a api to get all successors, predecessors of a class, paths, cycles and the like.

Class sets are sets of class names. They can be created by querying the dependency graph or manually by simply adding class names to the class set. A class name can be part of more than one class set. Class sets are the building blocks of ztest. You use them to create tests.

Tests are simple classes that are used to write junit-tests. They can fail, and if they fail they produce a List of testfailures. The list can be processed programmatically or can simply be output to xml, which is the default way of using ztest. Code a test, run, write failures as xml to the console, fix, run again.

ztest comes with a bunch of tests, but the most useful one is ZDependencyTest. This test allows you to specify a dependency graph between class sets by explicitly stating the allowed dependencies between class sets. If run against a class repository the test checks if any paths in the actual class dependencies violates the allowed dependencies. If so the violation is exposed as a test failure. If needed, you can export the allowed and actual dependencies to graphml, then you can layout and browse the graph with the free yEd graph editor.

The api is powerful and flexible, allowing the convenient extraction of information from the dependency graph. You can use it to extract many useful informations, like class hierarchy, implementations of interfaces, all classes (recursively) used by a class, classes used in fields etc. You can even check if a ear contains all needed classes, contains classes twice. You can tag jars in the archive manifest, and then compute the dependencies between the tagged jars, between packages, cycles and so on.

Sample diagram

Below is a sample package dependency graph created with ztest and layouted with yEd:

sample package dependency graph

 

 

Code sample

And here's a sample dependency test written with ztest:

//first scan the classpath
ZClassPathScanner scanner = new ZClassPathScanner();

//filter the scanned classes to avoid scanning all library jars
scanner.setFilter(new ZIClassPathFilter() {
public boolean acceptClass(String name)
throws Exception {
//only interested in my classes
return name.startsWith("com.myapp");
}

public boolean acceptClasspathPart(String name)
throws Exception {
//scan all
return true;
}

public boolean acceptResource(String resource)
throws Exception {
return false;
}
});

//what to scan
scanner.getClassPathItems().add(
new ZClassPathItemFile(new File("C:/path/to/myapp.ear")));

//use the factory to create classinfo
ZIClassInfoFactory factory = new ZClassInfoFactory(scanner);

//the main class, contains the extracted class information
//call is slow and needs a lot of memory,
//but its for unit tests not for runtime...
ZIClassInfo classInfo = factory.create();

//***************************************************
//define the class-sets for which to test dependencies
//***************************************************

ZIClassSet databaseClasses = new ZFilterClassSet("database", classInfo) {
public boolean accept(ZIClassInfo classInfo, String name)
throws Exception {
return name.startsWith("java.sql")
|| name.startsWith("javax.sql")
|| name.startsWith("net.sf.hibernate")
|| name.startsWith("javax.persistence");
}
};

ZIClassSet entityBeansClasses = new ZEntityBean3Set("entity-beans",
classInfo);

ZIClassSet statefulSessionBeansClasses =
new ZStatefulSessionBean3Set("stateful-session-beans",
classInfo);

ZIClassSet statelessSessionBeansClasses =
new ZStatelessSessionBean3Set("stateless-session-beans",
classInfo);

ZIClassSet renderClasses =
new ZClassSet("render-pojos",
classInfo.getClassesAnnotatedWith("org.ztemplates.render.ZRenderer"));

ZIClassSet actionClasses =
new ZClassSet("action-pojos",
classInfo.getClassesAnnotatedWith("org.ztemplates.actions.ZMatch"));

ZIClassSet localSessionBeanInterfaces =
new ZLocalSessionBean3InterfacesSet("local-session-bean-interfaces",
classInfo);

ZIClassSet remoteSessionBeanInterfaces =
new ZRemoteSessionBean3InterfacesSet("remote-session-bean-interfaces",
classInfo);


//***************************************************
//define the test that operates on the class-sets
//***************************************************

ZDependencyTest test = new ZDependencyTest(classInfo);

//add sets to the test
//(add nodes to the dependency graph)
test.addClassSet(databaseClasses);
test.addClassSet(entityBeansClasses);
test.addClassSet(statefulSessionBeansClasses);
test.addClassSet(statelessSessionBeansClasses);
test.addClassSet(localSessionBeanInterfaces);
test.addClassSet(remoteSessionBeanInterfaces);
test.addClassSet(renderClasses);
test.addClassSet(actionClasses);

//add allowed dependencies to the test
//(add edges to the dependency graph)

//stateful
test.addDependency(statefulSessionBeansClasses, databaseClasses);
test.addDependency(statefulSessionBeansClasses, entityBeansClasses);
test.addDependency(statefulSessionBeansClasses, localSessionBeanInterfaces);
test.addDependency(statefulSessionBeansClasses, remoteSessionBeanInterfaces);

//stateless
test.addDependency(statelessSessionBeansClasses, databaseClasses);
test.addDependency(statelessSessionBeansClasses, entityBeansClasses);
test.addDependency(statelessSessionBeansClasses, localSessionBeanInterfaces);
test.addDependency(statelessSessionBeansClasses, remoteSessionBeanInterfaces);

//actions
test.addDependency(actionClasses, localSessionBeanInterfaces);
test.addDependency(actionClasses, remoteSessionBeanInterfaces);
test.addDependency(actionClasses, renderClasses);

//run the test, will possibly accumulate failures
test.run();

//junit, toXML returns a xml failure description
//warning: xml format may change...
assertTrue(test.toXML(), test.getFailures().isEmpty());

 

This translates to the following dependency graph:

sample ztemplates.org architecture

 

Published at DZone with permission of its author, Gerd Ziegler.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)