Saturday, October 24, 2009

JExample: Defining Dependencies Between Unit Tests

I can't remember how I found it, but I've been meaning to investigate JExample for some time. To quote the project page, "JExample is a small framework to write unit tests that depend on each other."

I've read a lot about whether or not this is a good idea, and a lot of people say your unit tests should be able to run independent of one another. If one test fails, preventing the others to run, you might not catch all the failures in a complete run of your test suite. One test will fail, you'll fix it, and then you will be presented with a failure in another test.

After reading the project page, I can see where the project authors are coming from. They use a Stack as an example. Why you should run a test that adds three items to a Stack, when adding one item fails, or adding an item to an empty stack would fail? Maybe there's something to having dependencies between your tests. I'm not going to recommend either approach, for fear of starting another debate, but I would saying having the dependent approach in your toolbox could be useful depending on your particular testing situation.

The jar can be downloaded here: JExample Latest. Since I'm a Maven fan, I'll include how I added the jar to my Maven repository at the end of this post.

I do not want to reproduce the example found on the project page here, so I'll add a twist by writing my sample project in Groovy to see if it plays nicely (as I type, I haven't started).

My test project will be very similar though, a Rolodex class that keeps a list of Contacts

The Rolodex and Contact classes:

  class Contact {
def name
def phone
def email
}

class Rolodex {
def contacts = []
def add(contact) { contacts << contact }
def remove(contact) { contacts >> contact }
def size() { contacts.size() }
}
The start of my test class without anything from JExample:
  import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory

class RolodexTest extends GroovyTestCase {

Log logger = LogFactory.getLog(getClass())
def rolodex

void setUp() { rolodex = new Rolodex() }

void test_initialized_rolodex() {
logger.info "Test initial Rolodex."
assertEquals "rolodex empty", 0, rolodex.size()
}

void test_adding_first_contact() {
logger.info "Adding Cosmo."
def contact = new Contact(name:"Cosmo", phone:"x6469", email:"cosmo@mail.next")
rolodex.add contact
assertEquals "contact added", 1, rolodex.size()
}

void test_removing_first_contact() {
logger.info "Removing Cosmo."
def contact = new Contact(name:"Cosmo", phone:"x6469", email:"cosmo@mail.next")
rolodex.add contact
assertEquals "contact added", 1, rolodex.size()
rolodex.remove contact
assertEquals "rolodex now empty", 0, rolodex.size()
}
}

In every test, I have to start with a newly constructed Rolodex. The first test may seem trivial. Do I really need to test a List starts empty? I say why not, but at the very least it will help demonstrate adding dependencies between the tests. The key here is the third test, which essentially duplicates the second by requiring that I add a Contact in order to remove it.

The dependency chain flows naturally out of the tests in the order they are written. I can't create a Rolodex with one contact unless I start with an empty one, and I can't remove a Contact unless I have one to remove.

Ok, I'm ready to try some JExample. After a little trial and error, it turns out I need JUnit 4 so I can use the @RunWith annotation. Since I'm using JUnit 4, I no longer need to extend GroovyTestCase. At this point, it seems I'm forced to use the @Test annotation as well since I'm using the @RunWith annotation.

I also added the @Before annotation to my setUp() method. I won't reproduce the code here to save space, but with or without the @Before annotation, the setUp() method isn't being called. To keep going, I'll add a call to each test method for now, as I think after I define my dependencies the need for the method will go away anyway.

The test class after these changes looks like:

  import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.junit.Test
import org.junit.runner.RunWith
import static org.junit.Assert.assertEquals
import ch.unibe.jexample.JExample
import ch.unibe.jexample.Given

@RunWith(JExample)
class RolodexTest extends GroovyTestCase {

Log logger = LogFactory.getLog(getClass())

def rolodex

void setUp() { rolodex = new Rolodex() }

@Test void test_initialized_rolodex() {
logger.info "Test initial Rolodex."
setUp()
assertEquals "rolodex empty", 0, rolodex.size()
}

@Test void test_adding_first_contact() {
logger.info "Adding Cosmo."
setUp()
def contact = new Contact(name:"Cosmo", phone:"x6469", email:"cosmo@mail.next")
rolodex.add contact
assertEquals "contact added", 1, rolodex.size()
}

@Test void test_removing_first_contact() {
logger.info "Removing Cosmo."
setUp()
def contact = new Contact(name:"Cosmo", phone:"x6469", email:"cosmo@mail.next")
rolodex.add contact
assertEquals "contact added", 1, rolodex.size()
rolodex.remove contact
assertEquals "rolodex now empty", 0, rolodex.size()
}
}

And the tests pass. Note that the third test for removing a Contact is run second. As JUnit does not guarantee tests are run in the order they are defined, this should not come as a surprise:

  Running prystasj.jexample.RolodexTest
INFO: Test initial Rolodex.
INFO: Removing Cosmo.
INFO: Adding Cosmo.
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.981 sec

Now to try and reach for my goal here by adding dependencies between my tests. The @Given annotation is used to define dependencies. To make things more readable within the annotations, I'm also going to remove the test prefix at the start of my methods.

Skipping a bit ahead in my discovery, I was hoping the definition of the dependency chain would allow me to remove the addition of the Contact in the third test. Before I get to that, here's the new test class:

  @RunWith(JExample)
class RolodexTest {

Log logger = LogFactory.getLog(getClass())

def rolodex
def contact = new Contact(name:"Cosmo", phone:"x6469", email:"cosmo@mail.next")

void setUpEmptyRolodex() { rolodex = new Rolodex() }

@Test
void initialized_rolodex() {
logger.info "Test initial Rolodex."
setUpEmptyRolodex()
def rolodex = new Rolodex()
assertEquals "rolodex empty", 0, rolodex.size()
}

@Given("initialized_rolodex")
void add_first_contact () {
logger.info "Adding Cosmo."
rolodex.add contact
assertEquals "contact added", 1, rolodex.size()
}

@Given("add_first_contact")
void remove_first_contact() {
logger.info "Removing Cosmo."
logger.info "Rolodex size: ${rolodex.size()}"
rolodex.remove contact
assertEquals "rolodex now empty", 0, rolodex.size()
}
}

My tests pass without the addition of good ol' Cosmo in remove_first_contact. It does appear however that the dependencies are executed again for every test which can be seen in the output below. I added an additional logging statement to help verify the Rolodex has a size of 1 at the start of remove_first_contact:

  Running prystasj.jexample.RolodexTest
INFO: Test initial Rolodex.
INFO: Test initial Rolodex.
INFO: Adding Cosmo.
INFO: Test initial Rolodex.
INFO: Adding Cosmo.
INFO: Removing Cosmo.
INFO: Rolodex size: 1
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.016 sec

Method initialize_rolodex is run three times. While this may be considered a turn off, but if you're tests are fast as they should be, it shouldn't pose a problem. This might be working to my benefit here as I'm storing my Rolodex as a property of my test class. Before I get to that, let's fail the addition test and verify the removal test is not run by changing Rolodex.add(contact) to do nothing:

    def add(contact) { // do nothing }

After add_first_contact fails, the dependant, remove_first_contact, is skipped:

  Running prystasj.jexample.RolodexTest
INFO: Test initial Rolodex.
INFO: Test initial Rolodex.
INFO: Adding Cosmo.
Tests run: 3, Failures: 0, Errors: 1, Skipped: 1, Time elapsed: 1.014 sec <<< FAILURE!

Results :

Tests in error:
add_first_contact(prystasj.jexample.RolodexTest)

Tests run: 3, Failures: 0, Errors: 1, Skipped: 1
It looks I accomplished what I was after. There's no need to test removal, if addition doesn't work.

Back to storing my Rolodex as a property. I originally decided that was necessary so I could ensure the same Rolodex was being used in each test, but I have not made use of one of the features of JExample: The result of a test method can be injected into a method dependent on it as a parameter.

Making use of this, I can remove the rolodex property, but then I also have to make the tests in which others depend on return the altered Rolodex:

  @RunWith(JExample)
class RolodexTest {

Log logger = LogFactory.getLog(getClass())

def contact = new Contact(name:"Cosmo", phone:"x6469", email:"cosmo@mail.next")

@Test
Rolodex initialized_rolodex() {
logger.info "Test initial Rolodex."
def rolodex = new Rolodex()
assertEquals "rolodex empty", 0, rolodex.size()
rolodex
}

@Given("initialized_rolodex")
Rolodex add_first_contact(Rolodex rolodex) {
logger.info "Adding Cosmo."
rolodex.add contact
assertEquals "contact added", 1, rolodex.size()
rolodex
}

@Given("add_first_contact")
def remove_first_contact(Rolodex rolodex) {
logger.info "Removing Cosmo."
logger.info "Rolodex size: ${rolodex.size()}"
rolodex.remove contact
assertEquals "rolodex now empty", 0, rolodex.size()
}
}

There are a couple things to note here, although I'm using Groovy here, I have to explicitly add the return type of Rolodex to my test methods to get things to play nicely with JExample. Methods initialize_rolodex and add_first_contact return the Rolodex for use in the next test, but I've omitted the optional return keyword.


Maven & JExample

I was unable to find JExample in remote Maven repository, so I needed to add it manually to my repository. This makes sense as the project appears to come from a research group and there doesn't appear to be an official release of sorts (at the time of this writing the latest jar is titled r378). I'll use r378 as my version number for my Maven dependency instead of a more conventional release or snapshot version number.

The coordinates I'll use:

  <dependency>
<groupId>jexample</groupId>
<artifactId>jexample</artifactId>
<version>r378</version>
</dependency>

I considered using a more descriptive groupId that reflected the Software Composition Group, but I decided to keep things simple.

Install the jar to your local repository with:

  $ mvn install:install-file \
-DgroupId=jexample \
-DartifactId=jexample \
-Dversion=r378 \
-Dpackaging=jar \
-Dfile=jexample-r378.jar \
-DgeneratePom=true

You then should be able to include the jar as a dependency in your project using the dependency coordinates found above.


No comments:

Post a Comment