Sniper 101

May 24th, 2009

The Sniper uses JUnit, JMock, and the Boost IoC engine called the Spider. In this walk through, we are going to go through some of the features of a Sniper test, by explaining each moving part in the Sniper framework, and the Sniper lifecycle, as it applies to a Sniper test called FooTest for a simple test subject called DefaultFoo.

There are diagrams in the Appendices at the bottom of the page that provide an overview of the walk through.

The way that the Sniper works facilitates practices that we think are important when developing software. Things like maintaining 100% coverage, Test Driven Development, fine grained interfaces, boundary classes (we call them Edges), IoC, mocking, and automated testing. Not all of these ideas appeal to everyone (even really smart people), and we can accept that. Even if you don’t subscribe to all of these ideas, we still think you will find the Sniper useful.  

Okay. Here we go. Here is  some production code to test. We will call it the test subject

package org.boostalicious.sniper;

import au.net.netstorm.boost.bullet.log.Log;

public class DefaultFoo implements Foo {
    Baz baz;
    Log log;

    public boolean bar() {
        if (baz.valueOf() == true) {
            log.info("Foo'ed up bar");
            return true;
        } else {
            log.info("Still recognisable");
            return false;
        }
    }
}

It implements this interface…

package org.boostalicious.sniper;

public interface Foo {
    boolean bar();
}

Here is an example of the Sniper test for it….

package org.boostalicious.sniper;

import au.net.netstorm.boost.sniper.marker.*;
import au.net.netstorm.boost.spider.api.runtime.Nu;

import java.io.File;

public final class FooTest extends DemoTestCase
    implements OverlaysWeb, InjectableTest, LazyFields, HasFixtures, Destroyable {

    private static final File LOG = new File("build/artifacts/log/foo.log");
    private Foo subject;
    LogChecker logChecker;
    LogCleaner cleaner;
    Baz bazMock;
    Nu nu;

    public void overlay() {
        wire.ref(bazMock).one().to(Baz.class);
    }

    public void fixtures() {
        cleaner.scrub(LOG);
        subject = nu.nu(Foo.class);
    }

    public void testFoo() {
        runFoo(true, "Foo'ed up bar");
    }

    public void testNotFoo() {
        runFoo(false, "Still recognisable");
    }

    private void runFoo(boolean expected, String logging) {
        expect.oneCall(bazMock, expected, "valueOf");
        boolean actual = subject.bar();
        assertEquals(expected, actual);
        logChecker.checkLog(logging);
    }

    public void destroy() {
        cleaner.delete(LOG);
    }
}

 
We will go into more detail, but as a taster, the Sniper framework is performing the following things for us:

  • Using the Boost Spider to instantiate and inject the web of dependent objects that DefaultFoo requires.
  • Automatically creating a JMock proxy instance for bazMock, and using the mock proxy in place of real instances of Baz.
  • Automatically instantiating and injecting test fixtures such as, the Boost object factory nu, cleaner, and logChecker
  • Automatically calls methods such as overlay(), fixtures(), and destroy() at various points in the lifecycle of the test.

Compared to the test subject, the Sniper test is quite large. However, it is worth bearing in mind that you don’t need a new test class for every production class in order to test drive, or to achieve 100% coverage. In fact, it helps your maintainability if you have one test class for a cluster of classes.

We refer to these types of tests as MolecularTests. Correspondingly, a test that tests a single production class is called an AtomicTest. AtomicTests are typically harder to maintain due to the larger amount of dependency mocking of the test subject. For this reason, we tend to prefer MolecularTests.  

You might like to refer to a diagram we have made of the object graph of this example code. You can find it in APPENDIX A - Sniper 101 Object Graph at the bottom of the page.

Ok, first let’s look at the class definition…

public final class FooTest extends DemoTestCase
    implements OverlaysWeb, InjectableTest, LazyFields, HasFixtures, Destroyable {

 
You will see that it implements a bunch of interfaces. The Sniper test runner uses these interfaces as instructions about how to run the test. When we were first making the Sniper, we used object composition through extension and continually came up against problems (does anybody else think that “extends” is a really bad idea?) until someone hit upon (i.e. stole) the idea of using callback methods indicated via marker interfaces. They don’t help readability, but they do help test maintainability once you realise what is going on.  

There is a diagram that represents the default lifecycle of a Sniper Test in Appendix B - Sniper Lifecycle at the bottom of the page. You can change this lifecycle, and add your own marker interfaces, but we will talk about that another time. The diagram shows the name of the marker interfaces you implement to trigger some of the option steps in the lifecycle.  

OverlaysWeb - Implementing this marker interface will cause the test runner to call a method name overlay(). Special IoC wiring instructions are setup in overlay(). This is described in more detail below.

InjectableTest - This indicates that the test has some dependencies and that the Spider should automatically instantiate and inject them for us. This method is called after overlay(), so you can make use of overlay() wiring when the test itself is injected.

LazyFields - This bit is cool. If you declare any Object primitive as a dependency in your test, like Integer, Double, String etc., a random instance of that Object will be created for you. It even works with arrays. Furthermore, any mock proxy you need will be automagically created by appending “Mock” as a suffix to the field name. You can use the “Dummy” suffix to denote a mock that should have no methods called on it. Dummys are useful in ensuring the correct parameters are passed to test subject delegates.

HasFixtures - Implementing this marker will cause fixtures() to be called. This is called after overlay() is called, the lazy fields are instantiated and all other dependencies are injected. Here is where you would setup test fixtures and prepare for a test run. The example clears out a log file and instantiates the subject.

You will also note that the test extends DemoTestCase. The DemoTestCase is defined like this…

package org.boostalicious.sniper;

import au.net.netstorm.boost.sniper.core.LifecycleTestCase;
import au.net.netstorm.boost.sniper.web.SniperWeb;
import au.net.netstorm.boost.spider.ioc.BoostWeb;

public class DemoTestCase extends LifecycleTestCase {

    public Class[] webs() {
        return new Class[]{BoostWeb.class, SniperWeb.class, ScopeWeb.class};
    }
}

Spider allows you to compose webs of objects together. Near the beginning of the lifecycle of the test, the web() method is called to retrieve all the webs that are required to run the test. BoostWeb and SniperWeb are normally required, and people typically create a custom ScopeWeb when you first setup Sniper. In this super TestCase is also where you would add webs containing custom wiring that are used over and over again by your tests.    

Web composition in the Sniper 101 example

Web composition in the Sniper 101 example

We have defined the ScopeWeb like this…

package org.boostalicious.sniper;

import au.net.netstorm.boost.spider.api.config.web.Web;
import au.net.netstorm.boost.spider.api.config.mapping.Mapper;
import au.net.netstorm.boost.spider.api.config.scope.Scoper;

public class ScopeWeb implements Web {
    Mapper mappings;
    Scoper scoper;

    public void web() {
        mappings.prefix("Default", "org.boostalicious.sniper");
        scoper.scope("org.boostalicious.sniper");
    }
}

This means that the default Spider instantiation policy will be to try and instantiate an interface (X) with a Default(X) implementation for all injection candidates found in org.boostalicious.sniper. Using Mapper, you can specify different naming conventions for implementations in different parts of your code base. By using the Scoper you can restrict the scope of the Boost Spider to specific parts of your code base, enabling you to run many different IoC engines at once, if required. 

Armed with your knowledge of marker interfaces, the DemoTestCase, and a little bit about the Spider, let’s see how it all applies to the test dependencies that are declared as member variables in the test. Remember, this happens after the overlay() method is called…

    private static final File LOG = new File("build/artifacts/log/foo.log");
    private Foo subject; // private, therefore not injected.
    LogChecker logChecker; // An instance of DefaultLogChecker is injected.
    LogCleaner cleaner; // An instance of DefaultLogCleaner is injected.
    Baz bazMock; // A mock proxy for Baz injected.
    Nu nu; // An instance of DefaultNu is injected.

 
Right. Now its time for a bit of explaining about the overlay() method…

    // Called before this test is injected...
    public void overlay() {
        wire.ref(bazMock).one().to(Baz.class);
    }

As we have mentioned before, this method is called before anything is instantiated or injected, so it is a good place to put instructions about replacing or overlaying parts of the object dependency graph. 

sniper_overlay2

For example, say you had a test subject (X) that declared (Y) and (Z) as a dependencies and you wanted to test what would happen if you used the default implementation (Y) and its dependencies, and an implementation of (Z) that threw an exception under certain circumstances (like a database that was always full, or very slow for instance).

You would let the spider inject (Y) as normal and you would use the Spider wire field (declared in a superclass) and issue a wiring instruction in the overlay() method that replaced default implementations of (Z) with the exception throwing mock. The Sniper 101 example shows a mock called bazMock that will return true or false. This is a pretty standard object mocking scenario. There is better info about object mocking elsewhere. 

Nah-nah!… The fixtures() method…

    // Called after the test is injected...
    public void fixtures() {
        cleaner.scrub(LOG);
        // Because of the overlay, a new DeafultFoo will contain
        // bazMock instead of an instance of a DefaultBaz...
        subject = nu.nu(Foo.class);
    }

 
This is called after the test is injected but before each test is run. The example prepares a log file for reading after the subject is called. 

Exercising the Foo…

    private void runFoo(boolean expected, String logging) {
        // expect is an expectation field in a superclass...
        expect.oneCall(bazMock, expected, "valueOf");
        boolean actual = subject.bar();
        assertEquals(expected, actual);
        logChecker.checkLog(logging);
    }

 
As mentioned, you can expect the mock to return anything (including VOID), or throw an exception. Sniper exposes a bunch of JUnit methods that you can call to make assertions, just like you normally would. This part of the test is pretty standard.  

The destroy() method… 

    // Called after each test method...
    public void destroy() {
        cleaner.delete(LOG);
    }

This method is called after the each test method is called. The test runner will call destroy after the test is run regardless of whether or not an exception has been thrown by the test. For this reason, be wary of exceptions being thrown by destroy() itself as they may confuse the root cause of the test failure.  

THE END
So, that concludes the Sniper 101. The combination of the things we have described above allows us to write production code that, apart from employing IoC (which we think is a good idea for other reasons), is completely agnostic about code that tests it.

We hope that you can sort of see how you might be able to Test Drive and achieve 100% code coverage a little easier through the powerful (if not simple) way that the Sniper allows you to overlay portions of your object dependency graph with mocks, random Strings and Integers, and convenient fixture setup and destroy methods that can make use of dependency injection in both your test subject and the test itself.

APPENDIX A - Sniper 101 Object Graph

A graph describing the objects used in the Sniper 101

A graph describing the objects used in the Sinper 101

APPENDIX B - Sniper Lifecycle

A graph representing the various steps in the Sniper test lifecycle

A graph representing the various steps in the Sniper test lifecycle