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.
Een belangrijke regel voor tests is dat het mogelijk zou moeten zijn om onderdelen van je code onafhankelijk van elkaar te testen.
Als je bijvoorbeeld data leest uit een database, dan is die verbinding essentieel tijdens het testen. We zouden graag onafhanklijk willen zijn van die database verbinding.
Daarom ontwerpen we ons systeem zo, dat het gebruik maakt van een interface die toegan biedt tot de “databron”.
In het voorbeeld gebruikt ons MainProgram
de Student
klasse.
In plaats van de methoden om data in te lezen onderdeel te laten zijn van het MainProgram
, definieren we een StudentDataSource
interface met een aantal bruikbare methoden.
Zo’n interface scheidt het programma af van de werkelijken implementatie en biedt ons de mogelijkheid om een andere versie te gebruiken van een “StudentDataSource”:
Een aantal van deze implementaties bieden het programma wat eenvoudige data zodat ontwikkelaars snel verder kunnen. Een andere collega kan dan verder gaan met het bouwen van de database versie tegelijkertijd.
Maar de belangrijkste reden is degene die we eerder noemden:
Voor onze unittests willen we niet afhankelijk zijn van een database connectie. Het zou geweldig zijn als we onze applicatie kunnen testen zonder daarvan afhankelijk te zijn:
Door deze onafhankelijke verantwoordelijkheden kun je verschillende onderdelen testen wanneer de gehele implementatie nog niet beschikbaar is.
Daarnaast is het goed om specifieke onderdelen te testen zonder afhankelijk te zijn van de gehele applicatie.
We definieren een manager voor het bijhouden van een lijst van mensen zodat we verschillende dingen kunnen doen zoals een gemiddelde leeftijd berekeken. We willen dat deze methode gebruik maakt van de interne lijst die PeopleManager
bijhoudt.
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();
}
}
In plaats van het implementeren van het inladen mensen uit en JSON file gebruiken we een object dat de PeopleLoader
interface implementeerd:
PeopleLoader.java
public interface PeopleLoader() {
public List<Person> loadPeople();
}
Onze complete implementatie ziet er dan zo uit:
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;
}
}
De main applicatie kan er als volgt uit zien:
public static void main(String[] args) {
PeopleManager manager = new PeopleManager(new JSONPeopleLoader("people.json"));
System.out.println(manager.getAverageAge());
}
Maar om te testen gebruiken we een implementatie met dummy data:
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);
}
}
Zodat we op de volgende manier kunnen testen:
@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());
}