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
- Part 1: Unit Testing: Behavior vs State
- Part 2: Unit Testing and Dependency Injection
- Part 3: Don’t Let the Unit Tests Coverage Cheat you
- Part 4: Composition and Inheritance in Unit Testing (Soon)
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:

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:

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:

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.
[…] Part 4: Composition and Inheritance in Unit Testing […]
LikeLike
[…] Part 4: Composition and Inheritance in Unit Testing […]
LikeLike
[…] Part 4: Composition and Inheritance in Unit Testing […]
LikeLike