What are static methods?
- Static methods belong to the class rather than an instance of the class.
- Static methods are not inherited and thus cannot be overridden.
- You cannot use static methods for declaring a contract via an interface.
These facts are in themselves not bad.
Why are static methods bad for testing?
Let’s say we want to test the following createCommand method using JUnit Jupiter:
public class Mapper {
public Command createCommand(final Event event) {
return Command.builder()
.commandUuid(UUID.randomUUID().toString())
.id(event.getEventId())
.name(event.getName()).build();
}
}
@Builder
@Getter
@EqualsAndHashCode
public class Event {
private String type;
private String eventId;
private String name;
}
@Builder
@Getter
@EqualsAndHashCode
public class Command {
private String commandUuid;
private String name;
private String id;
}
But mocking the static UUID.randomUUID() method is not possible with most testing frameworks. This is where the problems starts:
We don’t know what to put as commandUuid because it get’s generated inside the mapping by using a static method. This dependency is hidden for us.
public class TestMapping {
@Test
public void testMapping() {
Mapper mapper = new Mapper();
String eventId = UUID.randomUUID().toString();
String name = "name";
Event event = Event.builder()
.eventId(eventId)
.name(name)
.type("create")
.build();
Command command = mapper.createCommand(event);
Command expected = Command.builder()
.name(name)
.id(eventId)
.commandUuid("Don't know what to do here? ?")
.build();
Assertions.assertEquals(command, expected);
}
}
So what could do we do instead?
- add dependencies for Mockito and Mockito JUnit Jupiter support
- Create UuidGenerator class with a generateUuid() method (if you want you could even go with an interface, but let’s keep it simple for now)
- inject UuidGenerator instance to the Mapper class.
- Mock the UuidGenerators generateUuid method to always return the same uuid.
Our code will now look like this:
public class UuidGenerator{
public String generateUuid(){
return UUID.randomUUID().toString()
}
}
public class Mapper {
private final UuidGenerator uuidGenerator;
public Mapper(UuidGenerator uuidGenerator) {
this.uuidGenerator = uuidGenerator;
}
public Command createCommand(final Event event) {
return Command.builder()
.commandUuid(uuidGenerator.generateUuid())
.id(event.getEventId())
.name(event.getName()).build();
}
}
@ExtendWith(MockitoExtension.class)
public class TestMapping {
private Mapper mapper;
@Mock
private UuidGenerator uuidGenerator;
@BeforeEach
public void before() {
mapper = new Mapper(uuidGenerator);
String commandId = UUID.randomUUID().toString();
Mockito.when(uuidGenerator.generateUuid()).thenReturn(commandId);
}
@Test
public void testMapping() {
String eventId = UUID.randomUUID().toString();
String name = "name";
Event event = Event.builder()
.eventId(eventId)
.name(name)
.type("create")
.build();
Command command = mapper.createCommand(event);
Command expected = Command.builder()
.name(name)
.id(eventId)
.commandUuid(uuidGenerator.generateUuid())
.build();
Assertions.assertEquals(command, expected);
}
}
One could argue that we could write a custom equalsData() function or something similar. Or check for all fields separately? But what if we’ll change one of the Event or Command classes? We would always need to think about adjusting these methods and imagine bigger data classes with lots of fields: everything would get very cluttered.
Apart from that this is just an example, this principle is applicable to many more cases.