Code with sideeffects is hard to test, hard to reason about. It is of course necessary to have sideeffects because otherwise all that happens is that our computer running a program generates heat. We do have to manage them though
The following are strategies that can make the code more testable, and allow control over the side effects
Use a command pattern
Consider splitting the code so that first ‘we decide what we are going to do’ and then ‘we do it’. This then allows the code that is doing the business logic of working out what sideeffects are needed to be tested separately from the ‘and now we do it’.
Use a layer of abstraction
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! 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
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
final static String metricName = "databaseMeter"
final static String sql = " <some sql>"
public void myMethod(){
jdbc.update()
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)
Ilogger log = makeMock(ILogger.class);
IJdbcUpdate jdbc = makeMock(IJdbcUpdate.class);
IMetrics metrics = makeMock(IMetrics.class);
MyClass myClass = new MyClass(log, jdbc, metrics);
myClass.myMethod();
verify(jdbc, times(1)).update(MyClass.sql)
verify (log, times(1)).log("Written to database")
verify(metrics, times(1).mark(MyClass.metricName))
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.