wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

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 on 10 January 2020 | Comments (0) | categories: Java UnitTesting

Comments

  1. No comments yet, be the first to comment