Composition and Inheritance in Unit Testing

Object Oriented Programming is a pretty useful paradigm to build software. Composition and inheritance are some of the best concepts this paradigm brings us.

However, depending on the problem and context, we should favor composition over inheritance.

In this post, I will show how composition and inheritance affect your unit testing strategy

TRY IT YOURSELF: You can find the source code of this post here.

Unit Testing Series

Problem Context

Well, let’s start with some context. We have the following User entity:

class User {
  private String id;
  private String name;

  public User(String id, String name) {
    this.id = id;
    this.name = name;
  }

  ...
}

As we want to control how the id is generated, we give that responsability to a generic Service interface. We think, we are going to reuse this interface over multiple services:

interface Service<T> {
  String generateId();

  T save(T t);
}

Now, as we think we are going to use the same strategy regarding the id generation, we create an abstract class:

abstract class AbstractService<T> implements
        Service<T> {
  @Override
  public String generateId() {
    return UUID.randomUUID().toString();
  }
}

NOTE: This is an abstract class due to we want to implement the generateId method, but not the save one. The implementation regarding the save method is going to be responsible by the children of this class.

As we can see, our generateId uses the UUID class to generate a new UUID each time we need it.

After, we define our first concrete implementation, a UserService:

class UserService extends AbstractService<User> {
  private final UserRepository repository;

  public UserService(UserRepository repository) {
    this.repository = repository;
  }

  @Override
  public User save(User user) {
    user.setId(generateId());

    return repository.save(user);
  }
}

There, we inherit from AbstractService, and use the generateId() method to create a new id when we are going to save a new User. Our design looks like this:

First design

Until now, we are awesome, we created interfaces, abstract classes, used generics, full OOP, we are genius.

Unit Testing The Whole Hierarchy

Let’s create an unit test to validate this “brilliant design”.

The first thing we notice is the method generateId in the AbstractService class, it uses a UUID.randomUUID() method that is static, so, as we don’t want to mock static things, we just extract that method to a new one, to spy it:

abstract class AbstractService implements
        Service {
  @Override
  public String generateId() {
    return getUUID().toString();
  }

  UUID getUUID(){
    return UUID.randomUUID();
  }
}

NOTE: Regarding the mocking to static things, you can check here for reasons to not do it.

Now, the strategy to test the save method for UserService class, is going to be to test the whole hierarchy, that means, we are going to test the save method with the generateId method:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
  @Mock
  private UserRepository userRepository;
  @InjectMocks
  @Spy
  private UserService userService;

  @Test
  public void save_newId_saved(){
    UUID uuid = UUID.randomUUID();

    User user = new User(null, "myName");
    User expectedUser = new User(uuid.toString(), "myName");

    doReturn(uuid).when(userService).getUUID();
    when(userRepository.save(expectedUser)).thenReturn(expectedUser);

    User newUser = userService.save(user);

    assertThat(newUser).isEqualTo(expectedUser);
  }
}

NOTE: This is a sociable unit test.

There, we just spied the getUUID() method to get a solid unit test.

NOTE: This approach is okey if that logic is simple. Besides, take into account that if you have severals AbstractService implementations, you will duplicate some test logic regarding the parent behavior.

Now, this strategy has the following advantages:

  • If the logic in the parent is simple, you save time and effort

Of course, some disadvantages:

  • If the logic in the parent is complex, you will need to work harder to test it.
  • If we have multiple concrete implementations, you will need to replicate the parent test logic in each of your concrete implementations tests.
  • If we cannot modify your parent to get it easy to test, you will get a lot of complex unit tests in each concrete class.
  • We cannot use the logic about id generation in other context outside of AbstractService class concrete implementations

Well, what if the generateId method is more complex, like accessing a database, or an external resource to create the id? we will want to mock it completely to isolate it and test it in a different class.

abstract class AbstractService implements
        Service {
  @Override
  public String generateId() {
    // COMPLEX LOGIC WE SHOULD TEST APART
  }
}

Testing Parent and Child Separately

Well, we can test the parent and the child apart, so, we split responsibilities.

Let’s start with the unit test for the UserService class:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
  @Mock
  private UserRepository userRepository;
  @InjectMocks
  @Spy
  private UserService userService;

  @Test
  public void save_newId_saved(){
    User user = new User(null, "myName");
    User expectedUser = new User("newId", "myName");

    doReturn("newId").when(userService).generateId();
    when(userRepository.save(expectedUser)).thenReturn(expectedUser);

    User newUser = userService.save(user);

    assertThat(newUser).isEqualTo(expectedUser);
  }
}

There, we spy the whole generateId method, we don’t care about UUID or similar, just about a String.

Now, let’s test the AbstractService class. Here, it is a little tricky, as the AbstractService class is abstract, we cannot instantiate in our unit tests to test it, so, we are going to create a NullObject concrete class first:

public class NullObjectAbstractService extends AbstractService {
  @Override
  public User save(User user) {
    return null;
  }
}

NOTE: We don’t care about the save method, we just need a concrete class to instantiate in our unit tests to test the generateId method.

This class just extends from AbstractService and let the save method blank. The design looks like this now:

Adding NullObject pattern

Now, we can create unit testing for the generateId method:

@RunWith(MockitoJUnitRunner.class)
public class AbstractServiceTest {
  @InjectMocks
  @Spy
  private NullObjectAbstractService nullObjectAbstractService;

  @Test
  public void generateId_UUID_generated(){
    UUID uuid = UUID.randomUUID();

    doReturn(uuid).when(nullObjectAbstractService).getUUID();

    String id = nullObjectAbstractService.generateId();

    assertThat(id).isEqualTo(uuid.toString());
  }
}

There, we use the new NullObjectAbstractService to test the generateId method, mocking the getUUID, and check if the result is ok.

Now, this strategy has the following advantages:

  • We can split responsibilities and isolated tests to get them more simple.
  • If the parent logic is complex, you can isolate those tests apart.
  • We don’t need to replicate the parent tests logic in each concrete implementation tests.

Of course, some disadvantages:

  • We will need to add another pattern like NullObject to test the parent.
  • We cannot use the logic about id generation in other context outside of AbstractService classes

Now, what if we have a new AbstractService implementation that doesn’t want to generate ids using the UUID class? well, we will need to override the generateId method in our concrete class.

What If I Need to Override Something?

Well, we want to override the generateId method to add a little sufix to the new id, so, our UserService class looks like this now:

class UserService extends AbstractService {
  private final UserRepository repository;

  public UserService(UserRepository repository) {
    this.repository = repository;
  }

  @Override
  public String generateId() {
    return super.generateId() + "+User";
  }

  @Override
  public User save(User user) {
    user.setId(generateId());

    return repository.save(user);
  }
}

As we can see, we override the generateId method, calling the super.generateId and adding the “User” string to the id.

Now, let’s test this new UserService logic, we need to mock the super.generateId method to create the unit testing:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
  @Mock
  private UserRepository userRepository;
  @InjectMocks
  @Spy
  private UserService userService;

  @Test
  public void save_newId_saved(){
    User user = new User(null, "myName");
    User expectedUser = new User("newId+User", "myName");

    doReturn("newId").when((AbstractService) userService).generateId();
    when(userRepository.save(expectedUser)).thenReturn(expectedUser);

    User newUser = userService.save(user);

    assertThat(newUser).isEqualTo(expectedUser);
  }
}

Do you think that the line 14 works? well, it doesn’t, Mockito doesn’t know which method to spy, the parent or the concrete one. So, we need to hack our code again, we extract the super invocation to another method, like this:

class UserService extends AbstractService {
  private final UserRepository repository;

  public UserService(UserRepository repository) {
    this.repository = repository;
  }

  @Override
  public String generateId() {
    return getId() + "+User";
  }

  String getId() {
    return super.generateId();
  }

  @Override
  public User save(User user) {
    user.setId(generateId());

    return repository.save(user);
  }
}

We created the getId method, abstracting the super.generateId invocation, so, we now can spy getId in our testing:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
  @Mock
  private UserRepository userRepository;
  @InjectMocks
  @Spy
  private UserService userService;

  @Test
  public void save_newId_saved(){
    User user = new User(null, "myName");
    User expectedUser = new User("newId+User", "myName");

    doReturn("newId").when(userService).getId();
    when(userRepository.save(expectedUser)).thenReturn(expectedUser);

    User newUser = userService.save(user);

    assertThat(newUser).isEqualTo(expectedUser);
  }
}

REMEMBER: We still need to create unit testing regarding the generateId method, so, we cannot avoid the NullObject pattern with this approach.

Now, with the overridden trouble, this strategy has the following advantages:

  • We can split responsibilities and isolated tests to get them more simple.
  • If the parent logic is complex, you can isolate those tests apart.
  • We don’t need to replicate the parent tests logic in each concrete implementation tests.

Of course, some disadvantages:

  • We will need to hack you overridden method to get it testable.
  • We will still need to add a NullObject to test the parent.
  • We cannot use the logic about id generation in other context outside of AbstractService classes

After all of those hacks, could we have a better option? well, if you use inheritance, not much, but, if you favor composition, things could get pretty easier

Favor Composition Over Inheritance

Now, let’s remove the inheritance and replace it with composition using the Strategy Pattern. First, we create a new interface (strategy), KeyGenerator:

interface KeyGenerator {
  String generateId();
}

This interface is pretty simple, just has the generateId method.

Now, let’s define a concrete implementation using the UUID class:

class UUIDKeyGenerator implements
        KeyGenerator {
  @Override
  public String generateId() {
    return getUUID().toString();
  }

  UUID getUUID() {
    return UUID.randomUUID();
  }
}

NOTE: Yeah, we still need to hack that UUID static method, but, it is okey, it is isolated to this class only.

Now, let’s create unit tests:

@RunWith(MockitoJUnitRunner.class)
public class UUIDKeyGeneratorTest {
  @InjectMocks
  @Spy
  private UUIDKeyGenerator uuidKeyGenerator;

  @Test
  public void generateId_UUID_generated() {
    UUID uuid = UUID.randomUUID();

    doReturn(uuid).when(uuidKeyGenerator).getUUID();

    String id = uuidKeyGenerator.generateId();

    assertThat(id).isEqualTo(uuid.toString());
  }
}

Next, we can see how the UserService looks like now:

class UserService implements
        Service {
  private final UserRepository repository;
  private final KeyGenerator keyGenerator;

  public UserService(
          UserRepository repository,
          KeyGenerator keyGenerator) {
    this.repository = repository;
    this.keyGenerator = keyGenerator;
  }

  @Override
  public User save(
          User user) {
    user.setId(keyGenerator.generateId());

    return repository.save(user);
  }
}

Now, we just have an aggregated dependency against the KeyGenerator from the UserService class, and we don’t specify the concrete implementation, that is the job of the Inversion of Control pattern. The design looks like this:

Favor composition over inheritance

Our unit tests for the UserService looks like this:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
  @Mock
  private UserRepository userRepository;
  @Mock
  private KeyGenerator keyGenerator;
  @InjectMocks
  private UserService userService;

  @Test
  public void save_newId_saved() {
    User user = new User(null, "myName");
    User expectedUser = new User("newId",
            "myName");

    when(keyGenerator.generateId()).thenReturn("newId");
    when(userRepository.save(expectedUser))
            .thenReturn(expectedUser);

    User newUser = userService.save(user);

    assertThat(newUser).isEqualTo(expectedUser);
  }
}

Now, this strategy has the following advantages:

  • We can split responsibilities and isolated tests to get them more simple.
  • We don’t need to replicate the parent tests logic in each concrete implementation tests.
  • We can have multiple concrete implementations and use them in multiple Services.
  • The design is simple.
  • We can use the KeyGenerator implementations in other context outside of Service classes
  • UserService class is totally decoupled regarding the KeyGenerator implementations.

Of course, some disadvantages:

  • We need to hack the static method, but, it is isolated to only one place.
  • We need more interfaces and classes to maintain.

Final Thought

Inheritance is beautiful and is one of the most valuable parts of Object Oriented Programming.

However, we should think twice if inheritance is the best option regarding our problem, moreover, regarding on how we are going to test it.

Composition is amazing, solving complex problems in isolated components, so, we should favor composition over inheritance.

If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.

Advertisement

3 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s