Dependency injection describes a design that allows a program to exchange one way of doing things with another. That way a part of the program that may take a little time to develop can be temporarily replaced with a version that solves the problem in a much simpler way.
An important rule for testing is that you should be able to test parts of your code indepenently.
When your code reads its data from a database for instanstance, then that connection is essential during testing. We would like to avoid any dependence on such a database connection.
To do so we design our system using an interface that gives us access to our “datasource”.
In the example our MainProgram
works with Student
s.
Rather than implementing methods to read the data directly in the MainProgram
, we define a StudentDataSource
interface with some methods that we find useful.
Such an interface separates the program from the actual implementation and allows us to exchange different versions of that “StudentDataSource”:
Some of these implementations provide the program with some simple data so that it can be developed further. Another colleague might be the person to actually implement the database version at the same time.
But the primary reason is the one we stated above:
To unit test some operations we may not actually want to connect to the database all the time. If we can test parts without that connection that would be great:
Through this separation of responsibilities you can test parts of the program even when the actual implementation is not always available to you.
Inversely it is always good to test the actual implementation without having to set up the entire application.
We have a manager that maintains a list of people and allows us to do various things such as getting the average age of all the people. We want this method to just use the internal list that PeopleManager maintains.
PeopleManager.java
public class PeopleManager {
List<Person> people;
public PeopleManager(PeopleLoader peopleLoader) {
people = peopleLoader.loadPeople();
}
public List<Person> getPeople() {
return people;
}
public int getAverageAge() {
return (int) loaded.stream().mapToInt(x -> x.getAge()).average().getAsDouble();
}
}
Rather than directly loading people from a JSON file we supply an object that implements the PeopleLoader
interface:
PeopleLoader.java
public interface PeopleLoader() {
public List<Person> loadPeople();
}
Our concrete implementation is then implemented as follows:
public class JSONPeopleLoader implements PeopleLoader{
String filePath;
public JSONPeopleLoader(String filePath) {
this.filePath = filePath;
}
@Override
public List<People> loadPeople() {
TypeReference<List<Person>> peopleTypes = new TypeReference<>(){};
ObjectMapper mapper = new ObjectMapper();
List<Person> loaded;
try {
loaded = mapper.readValue(new FileInputStream(filePath),peopleTypes);
} catch (IOException e) {
throw new RuntimeException(e);
}
return loaded;
}
}
In our main we would use this as following:
public static void main(String[] args) {
PeopleManager manager = new PeopleManager(new JSONPeopleLoader("people.json"));
System.out.println(manager.getAverageAge());
}
Now we can create a test using a dummy provided for testing:
public class DummyPeopleLoader implements PeopleLoader{
List<Person> dummies = new ArrayList<>();
@Override
public List<Person> loadPeople() {
return dummies;
}
public void addPerson(Person person) {
dummies.add(person);
}
}
And in our test we can do the following:
@Test
public void people_GetAverageAge_CorrectYearAverage() {
DummyPeopleLoader dummyPeopleLoader = new DummyPeopleLoader();
dummyPeopleLoader.add(new Person("FirstName", "LastName", 10));
dummyPeopleLoader.add(new Person("FirstName", "LastName", 20));
PeopleManager peopleManager = new PeopleManager(dummyPeopleLoader);
assertEquals(15, peopleManager.getAverageAge());
}