Saturday, 6 September 2014

Ninja, Injection and Fakes

The Ninja Framework has a convenient system for loading properties from a configuration file, application.conf. This includes a system of modes, where a property can be prefixed with one of three modes: dev, test or prod. So, when Ninja is running as a web server the configuration can easily be adapted to the three common situations, while keeping a single configuration file in the version control and CI process. This looks something like the following:

application.conf:
db.connection.readurl=jdbc:postgresql://localhost:5432/tripvis
db.connection.writeurl=jdbc:postgresql://localhost:5432/tripvis
db.connection.username=tripvis
db.connection.password=YeahRight

@Singleton
public abstract class PersistDB {

    @Inject
    NinjaProperties ninjaProperties;

    public static final String DB_READ_SERVER_URL_PROP = "db.connection.readurl";
    public static final String DB_READ_SERVER_URL_DEFAULT = "jdbc:postgresql://localhost:5432/tripvis";
    public static String dbReadServerUrl = DB_READ_SERVER_URL_DEFAULT;
    
    // etc

        @Inject
    protected void loadProps() throws BrokenSystemException {
        
        if (ninjaProperties != null) {
            dbReadServerUrl = ninjaProperties.getWithDefault(DB_READ_SERVER_URL_PROP, DB_READ_SERVER_URL_DEFAULT);
            // etc
        } else {
            logger.log(Level.SEVERE, "No NinjaProperties created, database config is stuffed");
        }
    }
}

The injection saves creating the object which deals with NinjaProperties and loading the file, which is very handy. However, there is a downside. When running JUnit tests the Ninja framework isn't loaded in the normal way, so the relevant classes aren't constructed by the framework but by the test object. (Yes, yes, I know, my example accesses a database, so this is stretching "unit" a bit.) This means that the injection no longer happens for free. Indeed, you're stuck with the hard-coded defaults. This is an issue in several ways:
  1. The test doesn't exercise the loading of the properties at all.
  2. If the properties required by the test environment change then the code has to change.
  3. If the developers' unit testing environment and the CI environment aren't the same then this approach stops working.
Fakes come, more or less, to the rescue. NinjaProperties is an interface, so we can create a fake properties object instead of the real one. This looks something like the following:

public abstract class PersistDB {
    public void fakeLoadProps(NinjaProperties props) throws BrokenSystemException {
        logger.log(Level.INFO, "loading fake properties");
        ninjaProperties = props;
        loadProps();
    }

public class UsersDBTest {
    static UsersDB db;

    @BeforeClass
    public static void createDBObject() throws BrokenSystemException {
        UsersDBTestFakeProps props;

        props = new UsersDBTestFakeProps();
        props.put("db.readserver", "localhost");
        props.put("db.writeserver", "localhost");
        props.put("db.connection.url", "jdbc:postgresql://localhost:5432/tripvis");
        props.put("db.connection.username", "dan");
        props.put("db.connection.password", "YoHoHo");
        db = new UsersDB();
        db.fakeLoadProps(props);
    }

public class UsersDBTestFakeProps implements NinjaProperties {

    private HashMap props;
    
    public UsersDBTestFakeProps() {
        props = new HashMap<>();
    }
    
    public void put(String key, String value) {
        props.put(key, value);
    }
    
    @Override
    public String get(String key) {
        return props.get(key);
    }

    @Override
    public String getWithDefault(String key, String defaultValue) {
        if (props.containsKey(key)) {
            return get(key);
        } else {
            return defaultValue;
        }
    }
// etc for any other methods needed for the tests

More work than injection, but mostly confined to the tests and not that tricksy.

Note, this is just a fake - a simple class that fulfils the interface, but takes a big short-cut. It is marginally more clever than the canned response from a stub, but doesn't go as far as checking that the right calls are made like a mock would. The mock approach would help test that the right queries are being made, but at the moment they're all bundled together in the loadProps() method so we're a good refactoring step away from that right now. For those that aren't familiar with the mock / fake / stub distinction I'd recommend reading Martin Fowler and Emily Bache.

Recalling the issues from before, I'm left with some caveats:
  1. The test doesn't touch the properties file at all. You still need to do some system testing to do that. But this approach flags an issue in code (rather than config) faster than that will.
  2. If the properties required by the test environment change then the test code has to change. Could be worse. If this is really an issue then the fake could load a file.
  3. If the developers' unit testing environment and the CI environment aren't the same then the code above needs tweaked to spot the environment and load different values into the fake. A bit fiddly and loading a file might start looking clever as soon as this gets at all involved, e.g. with more than one developer.
Final thought: Injection is kind of cool at runtime, but not without its own issues.

No comments:

Post a Comment