Unit Tests and Singletons
Good developers test their code. There are plenty of frameworks and tools around to make it less painful. Not every code is testable, so you write test first, to avoid getting stuck with untestable code. However there are situations where Unit Testing and pattern get into each other's way
The singleton and test isolation
I'm a big fan of design patterns, after all they are a well-proven solutions for specific known problems. E.g. the observer pattern is the foundation of reactive programming.
A common approach to implementing cache is the singleton pattern, that ensures all your code talks to the same cache instance ,independent of what cache you actually use: Aerospike, Redis, Guava, JCS or others.
Your singleton would look like this:
public enum TicketCache {
INSTANCE;
public Set<String> getTickets(final String systemId) {
Set<String> result = new HashSet<>();
// Your cache related code goes here
return result;
}
public TicketCache addTicket(final String systemId, final String ticketId) {
// Your cache/persistence code goes here
return this;
}
}
and a method in a class returning tickets (e.g. in a user object) for a user could look like this:
public Set<String> getUserTickets() {
Set<String> result = new HashSet<>();
Set<String> systemsResponsibleFor = this.getSystems();
systemsResponsibleFor.forEach(systemId ->
result.addAll(TicketCache.INSTANCE.getTickets(systemId)));
return result;
}
Now when you want to test this method, you have a dependency on TicketCache
and can't test the getUserTickets()
method in isolation. You are at the mercy of your cache implementation. But there is a better way
With a little refactoring your code can become more robust. In a nutshell: extract interfaces and make the cache injectable. Your cache after the operation looks like this:
public interface TicketCache {
public Set<String> getTickets(final String systemId);
public TicketCacheHolder addTicket(final String systemId, final String ticketId);
}
public enum TicketCacheHolder implements TicketCache {
INSTANCE;
@Override
public Set<String> getTickets(final String systemId) {
Set<String> result = new HashSet<>();
// Your cache related code goes here
return result;
}
@Override
public TicketCacheHolder addTicket(final String systemId, final String ticketId) {
// Your cache/persistence code goes here
return this;
}
}
Your user class then looks like this (non essentials left out):
public class User {
private TicketCache ticketCache = null;
public Set<String> getUserTickets() {
Set<String> result = new HashSet<>();
Set<String> systemsResponsibleFor = this.getSystems();
systemsResponsibleFor.forEach(systemId ->
result.addAll(this.getTicketCache().getTickets(systemId)));
return result;
}
public TicketCache getTicketCache() {
if (this.ticketCache == null) {
this.ticketCache = TicketCacheHolder.INSTANCE;
}
return this.ticketCache;
}
public void setTicketCache(final TicketCache ticketCache) {
this.ticketCache = ticketCache;
}
}
Why that? In normal operation, you want to use the cache and you don't want to break existing code by requiring a cache to be specified. So in production you actually never call setTicketCache
, so your User
class defaults to the singleton cache instance. In your test setup however, you would provide you "reliable cache". It could look like this:
class UserTest {
private Set<String> cacheContent;
private TicketCache cache;
@BeforeEach
void resetCache() {
this.cacheContent = new HashSet<>();
this.cacheContent.add("Red");
this.cacheContent.add("Green");
this.cache = new TicketCache() {
@Override
public Set<String> getTickets(String systemId) {
return this.cacheContent;
}
@Override
public TicketCacheHolder addTicket(String systemId, String ticketId) {
UserTest.this.cacheContent.add(ticketId);
return this;
}
};
}
@Test
void testGetUserTickets() {
User user = new User();
user.setTicketCache(cache);
Set<String> result = user.getUserTickets();
assertEquals(this.cacheContent, result, "I should get the exact cache content back");
}
}
And voila, a test in isolation. As usual YMMV
Posted by Stephan H Wissel on 10 January 2020 | Comments (1) | categories: Java UnitTesting