Let us suppose the side effect is ‘printing to a log’ or ‘sending a metric’. The following is a sample from drop wizard
private final static Logger log = Logger.getLogger(MyLogger.class.getName());
public void myMethod(){
Jdbc.update(" <some sql>")
log.info("Written to database")
Meter meter = metricRegistry.meter("databaseMeter");
meter.mark(1);
}
This is quite typical where we ‘do some database things’ (side effects) and in the same method log our results and notify the metrics system.
This code is really hard to test!
- How do we test the log: we need to make an adapter and instantiate the logging framework.
- How do we test the metrics: we need a whole metrics registry, and need to understand it’s lifecycle. Perhaps there are threads involved and all sorts of things.
- How do we test the Jdbc. Do we need to go to the database and check the sql ‘worked’?
Let’s work out how we could simplify it. This is not about ‘whether its ‘right to use sql or prepared statements’, just about how we test what we have.
- The Jdbc.update could be a call to a method on an interface called ‘IJdbcUpdate’
- The log.info should really be a call to an injected interface (we have a dependancy injection framework don’t we?)
- The meter could be a call on a ‘IMetrics’ interface
final static String metricName = "databaseMeter"
final static String sql = " <some sql>"
private final ILogger log; // configured by dependency injection
private final IJdbcUpdate jdbc; // our interface that decouples us from the JDBC framework we are using
private final IMetrics metrics; // our interface that decouples us from the metrics library we are using
public void myMethod() {
jdbc.update(sql);
log.info("Written to database");
metrics.mark(metricName, 1);
}
Observe that now the code is really easy to test. And further we have decoupled ourselves from the libraries that we are using: a double bonus.
The test would now look like (assuming constructor injection)
@Test
public void testHappyPathWritesToDatabaseAndProducesLogsMetrics() {
ILogger log = mock(ILogger.class);
IJdbcUpdate jdbc = mock(IJdbcUpdate.class);
IMetrics metrics = mock(IMetrics.class);
JdbcLoggingMetrics myClass = new JdbcLoggingMetrics(log, jdbc, metrics);
myClass.myMethod();
verify(jdbc, times(1)).update(sql);
verify(metrics, times(1)).mark(metricName, 1);
verify(log, times(1)).info("Written to database");
}
For extra points this could easily be split into three tests: one that checks the jdbc is called correctly, another that checks a log is called and another for the metrics.
After those tests we need to write some more
- Does the IJbdc.update method do what it is supposed to do?
- Does the IMetrics.mark method do what it is supposed to do?
- Does the log.info do what it is supposed to do
Previously all these tests had to be tested as part of the original code. Now we have decoupled them. If at some future date the metrics library or the logging library or the jbdc library are changed this test won’t be impacted. When we have other code using these interfaces they don’t need to test these methods.
Review
A few more tests are required for this method
- What if the Sql method throws an exception: what do we do? Are we happy with the behaviour of the metrics and log when this occurs?
- What if the metrics method throws an exception? Have we changed the database and are we happy with this? What do we log?
It’s at this moment we realise that the code is actually complicated and that our simplistic approach was flawed
- We need to log the exception.
- We need to write to a different metric is there was a problem
We can write the code to support this behavior in the code as it is, but it becomes complicated:
public void myMethod() {
try {
jdbc.update(sql);
log.info("Written to database");
metrics.mark(successfulMetricName, 1);
} catch (Exception e) {
log.error("Error writing to database", e);
metrics.mark(failedMetricName, 1);
}
}
And this doesn’t take into account what happens if the logging code throws an exception or the metrics code throws an exception. As we know anything in java can throw an exception (out of memory if nothing else)