Test development is a very simple task and if you've already done this for JUNIT, then doing it for TESTARE will come naturally.
Source code for a simple test case looks like this:
public class MockTestCase extends TestCase implements ClientSideTest {
/**
* <p>this method should be overwritten in specific test cases in order to register each individual decorator
*/
public void registerDecorators() {
registerFixture("fixture1", new Fixture1());
registerFixture("fixture2", new Fixture2());
registerProbe(new MyClientSideTestProbe());
registerProbe(new MyFloatingTestProbe());
}
/**
* should succeed
*/
public void testForSuccess() {
return;
}
/**
* should fail
*/
public void testFail() {
fail("because I want so");
}
/**
* should succeed
*/
public void testAssertTrueTrue() {
assertTrue("I think it's true", true);
}
/**
* should fail
*/
public void testAssertTrueFalse() {
assertTrue("I think it's true", false);
}
/**
* should succeed
*/
public void testAssertFalseFalse() {
assertFalse("I think it's false", false);
}
/**
* should fail
*/
public void testAssertFalseTrue() {
assertFalse("I think it's false", true);
}
/**
* this one should force an error
*/
public void testForError() {
throw new RuntimeException("just forced test to error");
}
}
We notice that a test case class must extend TestCase and can implement one or more execution scenarios. If no scenario is specified, the test case will be executed in the default scenario, which is the client JVM one.
By looking at the source code you can notice that test case behavior can be controlled using familiar fail(), assertTrue(...) and assertFalse(...) constructions.
The registerDecorators() method can be overloaded in any test case in order to register decorators such as fixtures, probes and guards.
Fixtures are test case decorators that help create the required test case environment and clean it up when the test is completed. Here is an example of a simple fixture:
public class Fixture1 extends Fixture implements ClientSideFixture {
public Fixture setUp() throws FixtureSetUpException {
return this;
}
public void tearDown(TestResultDescriptor testResultDescriptor) throws FixtureTearDownException {
}
}
Fixtures must extend Fixture and they can implement one execution scenario. Fixtures can be used with one special scenario: the floating scenario. The floating scenario makes sure the fixture is executed within the same environment as the test case.
The fixture's setUp method returns an instance of the fixture itself. Although this may seem strange, it can actually be very useful. Imagine the following use case: you must write a test case that tests an object's read method, which takes the object's primary key as a parameter. Of course you will write a fixture that creates an object in its setUp method and removes it in its tearDown method. The problem however is that the primary key is assigned to the object on the server side, which means it woudn't be available in your test case for the read method to use it. If, however, the fixture's setUp method returns the instance of the fixture itself, then the test case will receive a copy of the newly created object even if the fixture was executed in a remote environment.
Another particularity of TESTARE fixtures is that tearDown receives a TestResultDescriptor as a parameter, which informs the programmer of the outcome of the test. This is useful as quite often actions that need to be performed by this method depend on the outcome of the test, and if the test has failed, it also depends how the test has failed. As an example, let's analyze the test of a simple delete method. You will need a fixture that creates an object in the environment. Then the test will execute the delete method. Then a probe can be used to decide weather or not the object was successfully deleted – this would determine if the test has succeeded or not. Now we come to the point where the fixture's tearDown method must be called. If the test was successful, then the object's already gone from the database so the fixture doesn't have to do anything. If the test failed, then the tearDown method must take responsibility to delete the object and leave the environment in a clean state.
Global fixtures are just like regular fixtures, exept that instead of being assigned to test case and having the setUp and tearDown methods executed before and after each test, they are global and setUp methods and tearDown methods of all global fixtures are executed before the first test in the suite is executed and after the last test has been executed, respectively.
Here is an example of a simple global fixture, as it can be found in the distribution's samples directory:
public class FirstGlobalFixtureGlof extends GlobalFixture implements ClientSideGlobalFixture {
public void setUp() throws GlobalFixtureSetUpException {
}
public void tearDown() throws GlobalFixtureTearDownException {
}
}
Every global fixture must extend GlobalFixture and can implement one execution scenario. If the GlobalFixture doesn't implement an execution scenario, it will be executed in the default scenario, which is the client jvm.
Probes are test utilities that make sure the environment where the test case was executed was modified as expected. Probes are typically useful when the test case is executed in, or modifies remote environments. A test probe is like a spy: once instantiated it is injected in the environment where the test was executed, investigates this environment, decides if the environment is in the state in which it is expected to be, stores the results and then returns to the test case. The test case investigates the results and decides if the test was a success or not.
Here is an example of a simple global fixture, as it can be found in the distribution's samples directory:
package com.thekirschners.TESTARE.samples;
import com.thekirschners.TESTARE.core.TestProbe;
import com.thekirschners.TESTARE.core.runner.decorators.probes.TestProbeException;
public class SampleProbe extends TestProbe {
public void executeProbe() throws TestProbeException {
// todo: insert code to cheeck if the environment was altered as expected
}
}
The more complex your testing environment is, the more difficult it will be to manage it. It happens sometimes for some fixtures to be buggy and as a result the tearDown method would leave the environment in an incoherent state – like leave in the database objects that are unaccounted for and so on. Having such objects in your environment can compromise other test case results. For example, let's say we have a readAllPersons service that we need to test. But just before it, we've tested “createPerson” and the fixtures didn't properly remove the test object from the database. The readAllPersons test will probaly use a fixture to create a random number of objects in the database and then expect readAllPersons to return exactly the same number of objects and the objects to have exactly the same identity as those created by the fixture. But since the proviously executed test left a poluted environment, readAllPersons, if it functions properly, it will return one more object then it should and the test would fail, even though the service functions corectly. A guard registered with a test case could make sure tests leave the environment in a clean state after execution and this way we can prevent this kind of situations. This may seem a paranoid's tool, but the concept prooved very usefull to me. In various situations – especially with complicated integration tests when objects are created and distroyed in a variety of locations.
Visualizing the results of your unit tests is as important as actually running it. TESTARE has built in support for three types of reporting
direct to console reporting – this type of reporting is done as tests are executed. It's quite handy as you can see the results immediately but can get quite confusing if you have a big number of tests running.
HTML reporting – you can ask TESTARE to output test results in HTML form, by specifying it in the ant task. TESTARE uses Velocity to generate HTML content and as such the output is highly customizable.
XML output – results can be fed into an XML file which can then be transformed using XSL or fed into analisys tools or other applications.