Parasoft-Logo Suche

Entdecken Sie das TÜV-zertifizierte GoogleTest mit Agentic AI für C/C++-Tests!
Details ansehen »

Parasoft-Blog

Warum Sie Mocking jetzt zum Testen von Spring Boot-Einheiten verwenden sollten

By Brian McGlauflin • 5. Juni 2023 • 6 min gelesen
5. Juni 2023 | 6 min gelesen
By Brian McGlauflin
Text links: Warum Sie Mocking jetzt zum Testen von Spring Boot-Einheiten verwenden sollten. Auf der rechten Seite ist eine Nahaufnahme von Händen zu sehen, die auf einer Tastatur tippen. mit einer transparenten Codeüberlagerung.

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.

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.

Was bedeutet Spott im Spring Boot?

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.

Warum muss ich mich verspotten?

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:

Grafik, die die vielfältigen Abhängigkeiten eines Spring-Dienstes zeigt. Vom Controller zum Dienst und dann entweder zu einer Datenbank oder zu Bibliotheken.

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?

Die Vorteile des Spotts

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:

  • Sie müssen den speziellen „Test“-Code nicht selbst schreiben und pflegen.
  • Mocking-Bibliotheken können Aufrufe gegen Mocks verfolgen und bieten so eine zusätzliche Validierungsebene.
  • Standardbibliotheken wie Mockito bieten zusätzliche Funktionen, wie das Verspotten statischer Methoden, privater Methoden oder Konstruktoren.
  • Kenntnisse über eine Mocking-Bibliothek wie Mockito können projektübergreifend wiederverwendet werden, wohingegen Kenntnisse über benutzerdefinierten Testcode nicht wiederverwendet werden können.
Eine Grafik, die zeigt, wie ein simulierter Dienst mehrere Abhängigkeiten ersetzen kann. Der Controller geht zum Dienst oder zu einem simulierten Dienst. Der Dienst stellt auch eine Verbindung zu einer Datenbank und Bibliotheken her, während dies beim simulierten Dienst nicht der Fall ist.

Ein verspotteter Dienst ersetzt mehrere Abhängigkeiten

Abhängigkeiten im Frühjahr

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.

Wie fange ich an, Abhängigkeiten in einem Frühlingstest zu verspotten?

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:

Mocking-Testempfehlung in Parasoft Jtest

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.

Wie verspotte ich Abhängigkeiten in Spring Boot?

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 {
…
}
}

XML vs. Java-Konfiguration

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.

Verspotten statischer Methoden

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&amp;lt;Person&amp;gt; getPerson(@PathVariable("id") int id, Model model)
{
Person person = ExternalPersonService.getPerson(id);
if (person != null) {
return new ResponseEntity&amp;lt;Person&amp;gt;(person, HttpStatus.OK);
}
return new ResponseEntity&amp;lt;&amp;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:

Screenshot mit Parasoft Jtest

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&amp;lt;&amp;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.

Fazit

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.

Verbessern Sie Unit-Tests für Java mit Automatisierung: Best Practices für Java-Entwickler
Laden Sie das Ebook herunter