2.7. The JUnit test cases
ArgoUML has a set of automatic test cases using JUnit-framework
for testing the insides of the code.
The purpose of these are to help in pin-pointing problems with
code changes before even starting ArgoUML.
The JUnit test cases reside in a separate directory and
run from ant targets in
trunk/src/argouml-build/build.xml.
They are never distributed with ArgoUML; they are merely a tool for
developers.
By running the command
build tests guitests
or
build alltests
in trunk/src/argouml-build
these test cases are run, each in its own JVM.
Each test case writes its result on the Ant log.
The result is also generated into a browsable report that can be found at
build/test/reports/junit/output/html/index.html.
This is the same report that is presented as a result from
the nightly build.
The test cases' Java source code is located under
argouml/tests/org/argouml.
2.7.1. About the Test case Class
Now this will make all you Java enthusiasts go nuts!
We have both class names and method names with a special syntax.
The name of the test case class starts with "Test" (i.e. Capital T, then
small e, s and t) or
"GUITest" (i.e. Capital G, U, I, T then small e, s, t).
The reason for this is that the special targets in
trunk/src/argouml-build/build.xml
search for test case classes with these names.
If you write a test case class that does not comply to this rule, you still
can run the test cases in this class manually
but they wont be run by other developers and nightly build.
If you write support classes for the tests on the other hand, don't name
them as a test case to avoid confusion.
Test case classes that don't require GUI components in place
have filenames like
Test*.java.
They must be able to run on a headless system.
To make sure that this works, always run your newly developed
test cases with build tests.
[1]
Test case classes that do require GUI components in place
have filenames like
GUITest*.java.
Every test case class imports the JUnit framework:
import junit.framework.TestCase;
and it inherits TestCase
(i.e. junit.framework.TestCase).
2.7.2. Naming JUnit tests classes
An ArgoUML class
org.argouml.x.y.z
stored in the file
trunk/src/argouml-app/src/org/argouml/x/y/z.java
has its JUnit test case class called
org.argouml.x.y.Testz
stored in the file
trunk/src/argouml-app/tests/org/argouml/x/y/Testz.java
containing all the Unit Test Cases for that class
that don't need the GUI components to run.
Tests that
do need GUI components to run should be part of a class named
org.argouml.x.y.GUITestz
stored in the file
trunk/src/argouml-app/tests/org/argouml/x/y/GUITestz.java
If, for convenience reasons, you would like to split the tests of a single
class into several test classes, just name them with some extra suffix.
Either 1, 2, 3, or something describing what part that test case tests.
If you only want to run your newly written test cases and not
all the test cases, you could start with the command
build run-with-test-panel
and give the class name of your test case like
org.argouml.x.y.Testz
or
org.argouml.x.y.GUITestz.
You will then get the output in the window.
You could run all tests in this way by specifying the special test suite
org.argouml.util.DoAllTests
in the same way.
2.7.3. About the Test case Methods
Methods that are tests must have names that start with "test"
(i.e. all small t, e, s, t). This is a requirement of the JUnit
framework.
Try to keep the test cases as short as possible.
There is no need in cluttering them up just to beautify the output.
Prefer
// Example from JUnit FAQ
public void testIndexOutOfBoundsExceptionNotRaised()
throws IndexOutOfBoundsException {
ArrayList emptyList = new ArrayList();
Object o = emptyList.get(0);
}
over
public void testIndexOutOfBoundsExceptionNotRaised() {
try {
ArrayList emptyList = new ArrayList();
Object o = emptyList.get(0);
} catch (IndexOutOfBoundsException iobe) {
fail("Index out of bounds exception was thrown.");
}
}
because the code is shorter, easier to maintain and you get a better
error message from the JUnit framework.
A lot of times it is useful just to run the compiler to verify that
the signatures are correct on the interfaces. Therefore Linus has
thought it is a good idea to add methods called
compileTestStatics,
compileTestConstructors, and
compileTestMethods
that was thought to include correct calls to all static methods,
all public constructors, and all other public methods that are not
otherwise tested.
These methods are never called.
They serve as a guarantee that the public interface of a class will
never lose any of the functionality provided by its signature in an
uncontrolled way in just the same way as the test-methods serve as a
guarantee that no features will ever be lost.
Example 2.1. An example without Javadoc comments
package org.argouml.uml.ui;
import junit.framework.*;
public class GUITestUMLAction extends TestCase {
public GUITestUMLAction(String name) {
super(name);
}
public void setUp() throws Exception {
super.setUp();
InitializeModel.initializeDefault();
}
// Testing all three constructors.
public void testCreate1() {
UMLAction to = new UMLAction(new String("hexagon"));
assert("Disabled", to.shouldBeEnabled());
}
public void testCreate2() {
UMLAction to = new UMLAction(new String("hexagon"), true);
assert("Disabled", to.shouldBeEnabled());
}
public void testCreate3() {
UMLAction to = new UMLAction(new String("hexagon"), true, UMLAction.NO_ICON);
assert("Disabled", to.shouldBeEnabled());
}
}
and the corresponding no-GUI-class:
package org.argouml.uml.ui;
import junit.framework.*;
public class TestUMLAction extends TestCase {
public TestUMLAction(String name) {
super(name);
}
// Functions never actually called. Provided in order to make
// sure that the static interface has not changed.
private void compileTestStatics() {
boolean t1 = UMLAction.HAS_ICON;
boolean t2 = UMLAction.NO_ICON;
UMLAction.getShortcut(new String());
UMLAction.getMnemonic(new String());
}
private void compileTestConstructors() {
new UMLAction(new String());
new UMLAction(new String(), true);
new UMLAction(new String(), true, true);
}
private void compileTestMethods() {
UMLAction to = new UMLAction(new String());
to.markNeedsSave();
to.updateEnabled(new Object());
to.updateEnabled();
to.shouldBeEnabled();
}
public void testDummy() { }
}
2.7.4. Improving a test case
Test cases are better if they are simpler.
Strive to involve as little part of the ArgoUML code as possible.
Ideally you are just testing a single class at a time.
The involvement of the Model subsystem is in most cases inevitable
since a majority of the classes within ArgoUML use the Model subsystem.
Nevertheless, we should, to allow for better and quicker tests,
strive to not involve the Model subsystem if possible.
If the Model subsystem is to be involved, it must be initialized.
Either with the default implementation (the MDR) or with some other
implementation.
For testing purposes there exists a Mock implementation that can
be used if no functionality is required from the Model subsystem or
when testing the Model subsystem itself.
If the Mock model subsystem implementation cannot be used,
then the tests have to have the MDR subproject on the class path
when running.
This is not a problem when running the tests from the ant setup
since MDR is always included when running the tests.
When running tests from within Eclipse this is a small problem.
[2]
This means that you should have the following priorities:
Don't use the Model subsystem.
Only possible in a few simple cases.
Use the Mock model subsystem implementation.
Call the function
org.argouml.model.InitializeModel#initializeMock()
from setUp() in your test case.
Only possible in a few simple cases.
Use the real Model subsystem implementation from the
setUp() function.
Call the function
org.argouml.model.InitializeModel#initializeDefault()
from setUp() in your test case.
Use the real Model subsystem implementation from the constructor.
Call the function
org.argouml.model.InitializeModel#initializeDefault()
in the constructor of your test case.
This is needed if your tests rely on the value in some member variables
referencing some object retrieved from the model.
Use the real Model subsystem implementation from the static initialization
section of your test class.
Call the function
org.argouml.model.InitializeModel#initializeDefault()
in the static initialization.
This is needed if your tests rely on the value
of some static member variables referencing
some object retrieved from the model.
We should try to get as many tests from a GUITest* class to
the corresponding Test* class because
The Test*-classes don't involve the GUI components and
are run by automatic builds regularly.