Entdecken Sie das TÜV-zertifizierte GoogleTest mit Agentic AI für C/C++-Tests!
Details ansehen »
Parasoft-Blog
Das Schreiben von JUnit-Tests für Ihre Spring-Anwendungen wird durch die Testinfrastruktur erleichtert, die das Spring Framework und Spring Boot bieten. Lesen Sie diesen von Experten zusammengestellten Beitrag durch, um mehr zu erfahren.
Zum Abschnitt springen
Das Spring Framework bietet zusammen mit Spring Boot ein hilfreiches Testframework zum Schreiben von JUnit-Tests für Ihre Spring-Anwendungen. In diesem Beitrag gehe ich auf eine der größten Herausforderungen beim Testen komplexer Anwendungen ein: das Abhängigkeitsmanagement.
Mocking ist eine Technik, die beim Unit-Testing eingesetzt wird , um das Verhalten realer Objekte zu simulieren, wenn die zu testende Einheit externe Abhängigkeiten aufweist. Die Simulationen, auch Mocks genannt, ersetzen die realen Objekte. Ziel des Mockings ist es, den Fokus auf den zu testenden Code zu legen und nicht auf das Verhalten oder den Zustand externer Abhängigkeiten.
Lass uns ehrlich sein. Komplexe Anwendungen werden nicht von Grund auf neu erstellt. Sie verwenden Bibliotheken, APIs und Kernprojekte oder -dienste, die von einer anderen Person erstellt und verwaltet werden. Als Spring-Entwickler nutzen wir vorhandene Funktionen so weit wie möglich, damit wir unsere Zeit und Mühe auf das verwenden können, was uns wichtig ist: die Geschäftslogik unserer Anwendung. Wir überlassen die Details den Bibliotheken, sodass unsere Anwendungen viele Abhängigkeiten aufweisen, die unten in Orange dargestellt sind:
Ein Spring-Service mit mehreren Abhängigkeiten
Wie kann ich Unit-Tests also auf meine Anwendung (Controller und Dienst) konzentrieren, wenn der Großteil ihrer Funktionalität vom Verhalten dieser Abhängigkeiten abhängt? Führe ich am Ende nicht immer Integrationstests statt Unit-Tests durch? Was passiert, wenn ich eine bessere Kontrolle über das Verhalten dieser Abhängigkeiten benötige oder die Abhängigkeiten während des Komponententests nicht verfügbar sind?
Ich benötige eine Möglichkeit, meine Anwendung von diesen Abhängigkeiten zu isolieren , damit ich mich bei meinen Unit-Tests auf den Anwendungscode konzentrieren kann. In manchen Fällen könnten wir spezielle „Testversionen“ dieser Abhängigkeiten erstellen. Die Verwendung einer standardisierten Bibliothek wie Mockito bietet jedoch aus mehreren Gründen Vorteile gegenüber diesem Ansatz:
Ein verspotteter Dienst ersetzt mehrere Abhängigkeiten
Im Allgemeinen teilen Spring-Anwendungen die Funktionalität in Beans auf. Ein Controller kann von einem Service Bean abhängen, und das Service Bean kann von einem EntityManager, einer JDBC-Verbindung oder einem anderen Bean abhängen. In den meisten Fällen handelt es sich bei den Abhängigkeiten, von denen der zu testende Code isoliert werden muss, um Beans. Bei einem Integrationstest ist es sinnvoll, dass alle Schichten real sind. Aber für Unit-Tests müssen wir entscheiden, welche Abhängigkeiten real und welche simuliert sein sollen.
Mit Spring können Entwickler Beans entweder mit XML, Java oder einer Kombination aus beiden definieren und konfigurieren, um eine Mischung aus verspotteten und echten Beans in Ihrer Konfiguration bereitzustellen. Da Mock-Objekte in Java definiert werden müssen, sollte eine Konfigurationsklasse verwendet werden, um die Mock-Beans zu definieren und zu konfigurieren.
Ein automatisiertes Testtool wie der Unit Test Assistant (UTA) von Parasoft Jtest unterstützt Sie bei der Erstellung aussagekräftiger Unit-Tests, die die Funktionalität Ihrer Spring-Anwendungen prüfen. Beim Generieren eines Spring-Tests durch UTA werden alle Abhängigkeiten Ihres Controllers als Mocks eingerichtet, sodass jeder Test die Kontrolle über die Abhängigkeit erhält. Während der Testausführung erkennt UTA Methodenaufrufe auf Mock-Objekten für Methoden, für die noch kein Methoden-Mocking konfiguriert ist, und empfiehlt, diese Methoden zu mocken. Anschließend können wir eine Schnellkorrektur verwenden, um jede Methode automatisch zu mocken.
Hier ist ein Beispiel-Controller, der von einem PersonService abhängt :
@Controller
@RequestMapping("/people")
public class PeopleController {@Autowired
protected PersonService personService;@GetMapping
public ModelAndView people(Model model){
for (Person person : personService.getAllPeople()) {
model.addAttribute(person.getName(), person.getAge());
}
return new ModelAndView("people.jsp", model.asMap());
}
}
Und ein Beispieltest, der vom Unit Test Assistant von Parasoft Jtest erstellt wurde:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class PeopleControllerTest {@Autowired
PersonService personService;MockMvc mockMvc;
// Other fields and setup
@Configuration
static class Config {// Other beans
@Bean
public PersonService getPersonService() {
return mock(PersonService.class);
}
}
@Test
public void testPeople() throws Exception {
// When
ResultActions actions = mockMvc.perform(get("/people"));
}
}
Der Test verwendet hier eine mit @Configuration annotierte innere Klasse, die Bean-Abhängigkeiten für den zu testenden Controller mittels Java-Konfiguration bereitstellt. Dadurch können wir den PersonService in der Bean-Methode mocken . Da noch keine Methoden gemockt sind, erhalte ich beim Ausführen des Tests die folgende Empfehlung:
Das bedeutet, dass die Methode `getAllPeople()` meines simulierten `PersonService` aufgerufen wurde , der Test das Mocking für diese Methode aber noch nicht konfiguriert hat. Wenn ich die Schnellkorrekturoption „Mock it“ auswähle, wird der Test aktualisiert.
@Test
public void testPeople() throws Exception {
Collection<Person> getAllPeopleResult = new ArrayList<Person>();
doReturn(getAllPeopleResult).when(personService).getAllPeople();
// When
ResultActions actions = mockMvc.perform(get("/people"));
Beim erneuten Ausführen des Tests ist er erfolgreich. Ich muss zwar noch die von getAllPeople() zurückgegebene Collection befüllen , aber das Problem mit der Einrichtung meiner simulierten Abhängigkeiten ist gelöst.
Beachten Sie, dass ich die generierte Verspottungsmethode von der Testmethode in die Bean-Methode der Konfigurationsklasse verschieben könnte. Wenn ich das mache, bedeutet das, dass jeder Test in der Klasse dieselbe Methode auf dieselbe Weise verspottet. Wenn Sie die Methode in der Testmethode verspotten lassen, kann die Methode zwischen verschiedenen Tests unterschiedlich verspottet werden.
Spring Boot vereinfacht das Bean-Mocking erheblich. Anstatt im Test ein `@Autowired`- Feld für die Bean und eine zugehörige `Configuration`-Klasse zu verwenden, genügt es, ein Feld für die Bean zu definieren und es mit `@MockBean` zu annotieren . Spring Boot erstellt dann mithilfe des im Klassenpfad verfügbaren Mocking-Frameworks ein Mock-Objekt für die Bean und injiziert es wie jede andere Bean im Container.
Bei der Generierung von Spring Boot-Tests mit dem Unit Test Assistant wird die @MockBean- Funktionalität anstelle der Configuration-Klasse verwendet.
@SpringBootTest
@AutoConfigureMockMvc
public class PeopleControllerTest {
// Other fields and setup – no Configuration class needed!@MockBean
PersonService personService;
@Test
public void testPeople() throws Exception {
…
}
}
Im ersten Beispiel oben hat die Konfigurationsklasse alle Beans für den Spring-Container bereitgestellt. Alternativ können Sie anstelle der Konfigurationsklasse die XML-Konfiguration für den Test verwenden. oder Sie können die beiden kombinieren. Zum Beispiel:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/**/testContext.xml" })
public class PeopleControllerTest {@Autowired
PersonService personService;// Other fields and setup
@Configuration
static class Config {
@Bean
@Primary
public PersonService getPersonService() {
return mock(PersonService.class);
}
}
// Tests
}
Die Klasse verweist hier über die Annotation `@ContextConfiguration` (hier nicht dargestellt) auf eine XML-Konfigurationsdatei , um die meisten Beans bereitzustellen. Dabei kann es sich um reale oder testspezifische Beans handeln. Zusätzlich wird eine `@Configuration`- Klasse bereitgestellt, in der `PersonService` simuliert wird. Die Annotation `@Primary` gibt an, dass der Test selbst dann die simulierte Bean aus der `@Configuration`- Klasse verwendet, wenn in der XML-Konfiguration eine `PersonService`- Bean gefunden wird . Diese Art der Konfiguration kann den Testcode schlanker und übersichtlicher gestalten.
Sie können UTA so konfigurieren, dass Tests mithilfe beliebiger @ContextConfiguration- Attribute generiert werden.
Manchmal wird statisch auf Abhängigkeiten zugegriffen. Beispielsweise könnte eine Anwendung über einen statischen Methodenaufruf auf einen Drittanbieterdienst zugreifen:
public class ExternalPersonService {
public static Person getPerson(int id) {
RestTemplate restTemplate = new RestTemplate();
try {
return restTemplate.getForObject("http://domain.com/people/" + id, Person.class);
} catch (RestClientException e) {
return null;
}
}
}
In unserem Controller:
@GetMapping
public ResponseEntity&lt;Person&gt; getPerson(@PathVariable("id") int id, Model model)
{
Person person = ExternalPersonService.getPerson(id);
if (person != null) {
return new ResponseEntity&lt;Person&gt;(person, HttpStatus.OK);
}
return new ResponseEntity&lt;&gt;(HttpStatus.NOT_FOUND);
}
In diesem Beispiel verwendet unsere Handler-Methode einen statischen Methodenaufruf, um ein Person-Objekt von einem Drittanbieterdienst abzurufen. Wenn wir einen JUnit-Test für diese Handler-Methode erstellen, wird bei jeder Testausführung ein echter HTTP-Aufruf an den Dienst durchgeführt.
Stattdessen simulieren wir die statische Methode ` ExternalPersonService.getPerson()` . Dadurch wird der HTTP-Aufruf verhindert und wir erhalten eine Antwort mit einem `Person` -Objekt, das unseren Testanforderungen entspricht. Der Unit-Test-Assistent erleichtert das Simulieren statischer Methoden mit Mockito.
UTA generiert einen Test für die oben beschriebene Handler-Methode, der folgendermaßen aussieht:
@Test
public void testGetPerson() throws Throwable {
// When
Int id = 1L;
ResultActions actions = mockMvc.perform(get("/people/" + id));
// Then
actions.andExpect(status().isOk());
}
Wenn wir den Test ausführen, sehen wir den HTTP-Aufruf im UTA-Flow-Tree. Suchen wir den Aufruf von ExternalPersonService.getPerson() und simulieren wir ihn stattdessen:
Der Test wurde aktualisiert, um die statische Methode für diesen Test mithilfe von Mockito zu simulieren:
@Test
public void testGetPerson() throws Throwable {
MockedStatic<ExternalPersonService>mocked = mockStatic(ExternalPersonService.class);
mocks.add(mocked);Person getPersonResult = null; // UTA: default value
mocked.when(()->ExternalPersonService.getPerson(anyInt())).thenReturn(getPersonResult);// When
int id = 1;
ResultActions actions = mockMvc.perform(get("/people/" + id));// Then
actions.andExpect(status().isOk());}
Set<AutoCloseable> mocks = new HashSet&lt;&gt;();
@After
public void closeMocks() throws Throwable {
for (AutoCloseable mocked : mocks) {
mocked.close();
}
}
Die Mockito -Methode `mockStatic` erstellt ein statisches Mock-Objekt für die Klasse, über das spezifische statische Aufrufe konfiguriert werden können. Um sicherzustellen, dass dieser Test andere Tests im selben Durchlauf nicht beeinträchtigt, müssen `MockedStatic`-Objekte nach Testende geschlossen werden. Daher werden alle Mock-Objekte in der Methode `closeMocks()` geschlossen , die der Testklasse hinzugefügt wird.
Mithilfe von UTA können wir nun die Variable getPersonResult auswählen und instanziieren, sodass der simulierte Methodenaufruf nicht null zurückgibt :
String name="";//UTA:default value
int age=0;//UTA:default value
Person getPersonResult=newPerson(name, age);
Wenn wir den Test erneut ausführen, wird getPersonResult von der simulierten ExternalPersonService.getPerson()- Methode zurückgegeben und der Test ist erfolgreich.
Hinweis: Im Ablaufdiagramm können Sie für statische Methodenaufrufe auch das Muster „Mockbare Methode hinzufügen“ auswählen. Dadurch wird der Unit-Test-Assistent so konfiguriert, dass diese statischen Methodenaufrufe beim Generieren neuer Tests immer gemockt werden.
Komplexe Anwendungen weisen häufig funktionale Abhängigkeiten auf, die das Unit-Testing des Codes erschweren und einschränken. Ein Mocking-Framework wie Mockito hilft Entwicklern, den zu testenden Code von diesen Abhängigkeiten zu isolieren und so schneller bessere Unit-Tests zu schreiben. Die Java-Produktivitätslösung von Parasoft vereinfacht das Abhängigkeitsmanagement, indem sie neue Tests für die Verwendung von Mocks konfiguriert, fehlende Methoden-Mocks zur Laufzeit findet und Entwicklern hilft, entsprechende Mocks zu generieren.