Empfohlenes Webinar: MISRA C++ 2023: Alles, was Sie wissen müssen | Zum Video

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

Kopfschuss von Brian McGlauflin,
5. Juni 2023
7 min lesen

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.

Der Frühling Rahmenbietet zusammen mit Spring Boot ein hilfreiches Test-Framework für Schreiben von JUnit-Tests für Ihre Frühlingsanwendungen. 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?

Spott ist ein Technik, die beim Unit-Testen verwendet wird um das Verhalten realer Objekte zu simulieren, wenn die getestete Einheit externe Abhängigkeiten aufweist. Die Simulationen oder Mocks werden anstelle der realen Objekte verwendet. Das Ziel des Mockings besteht darin, den getesteten Code zu isolieren und sich auf ihn zu konzentrieren 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

Was ich brauche, ist ein Weg dazu Isolieren Sie meine Anwendung von diesen Abhängigkeiten, sodass ich meine Komponententests auf meinen Anwendungscode konzentrieren kann. In einigen 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 kann Ihnen dabei helfen, aussagekräftige Unit-Tests zu erstellen, die die Funktionalität Ihrer Spring-Anwendungen testen. Wenn UTA einen Spring-Test generiert, werden alle Abhängigkeiten für Ihren Controller als Mocks angelegt, sodass jeder Test die Kontrolle über die Abhängigkeit erhält. Beim Ausführen des Tests erkennt UTA Methodenaufrufe auf einem Mock-Objekt für Methoden, für die noch kein Methoden-Mocking konfiguriert ist, und empfiehlt, diese Methoden zu verspotten. Wir können dann eine schnelle Lösung verwenden, um jede Methode automatisch zu verspotten.

Hier ist ein Beispiel-Controller, der von a abhängt PersonenService:

@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"));
    }
}

Hier verwendet der Test eine innere Klasse, die mit annotiert ist @AufbauHiermit werden Bean-Abhängigkeiten für den zu testenden Controller mithilfe der Java-Konfiguration bereitgestellt. Dies ermöglicht es uns, das zu verspotten PersonenService in der Bohnenmethode. Es sind noch keine Methoden verspottet. Wenn ich den Test durchführe, wird folgende Empfehlung angezeigt:

Dies bedeutet, dass die getAllPeople () Methode wurde auf meine verspottete aufgerufen PersonenService, aber der Test konfiguriert Mocking für diese Methode noch nicht. Wenn ich die Schnellkorrekturoption „Mock it“ wä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"));

Wenn ich den Test erneut durchführe, besteht er. Ich sollte immer noch die bevölkern Sammlung das wird zurückgegeben von getAllPeople (), aber die Herausforderung, meine verspotteten Abhängigkeiten einzurichten, 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 macht das Verspotten von Bohnen noch einfacher. Anstatt eine zu verwenden @Autowired Feld für die Bean im Test und eine Konfigurationsklasse, die sie definiert. Sie können einfach ein Feld für die Bean verwenden und mit Anmerkungen versehen @ MockBean. Spring Boot erstellt mithilfe des im Klassenpfad gefundenen Mock-Frameworks einen Mock für die Bean und injiziert ihn auf die gleiche Weise, wie jede andere Bean im Container injiziert werden kann.

Beim Generieren von Spring Boot-Tests mit dem Unit Test Assistant wird der @ MockBean Die Funktionalität wird anstelle der Konfigurationsklasse 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
}

Hier verweist die Klasse auf eine XML-Konfigurationsdatei in der @ContextConfiguration Anmerkung (hier nicht gezeigt), um die meisten Bohnen bereitzustellen, bei denen es sich um echte Bohnen oder testspezifische Bohnen handeln kann. Wir bieten auch eine @Aufbau Klasse, wo PersonenService wird verspottet. Das @Primär Anmerkung gibt an, dass auch wenn a PersonenService Bean befindet sich in der XML-Konfiguration. Bei diesem Test wird die verspottete Bean aus dem verwendet @Aufbau Klasse stattdessen. Diese Art der Konfiguration kann das Testen von Code verkleinern und die Verwaltung vereinfachen.

Sie können UTA so konfigurieren, dass Tests mit einer bestimmten Methode generiert werden @ContextConfiguration Attribute, die Sie benötigen.

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.

Verspotten wir stattdessen die Statik ExternalPersonService.getPerson () Methode. Dies verhindert den HTTP-Aufruf und ermöglicht uns die Bereitstellung eines Person Objektantwort, die unseren Testanforderungen entspricht. Der Unit-Test-Assistent kann es einfacher machen, statische Methoden mit Mockito zu verspotten.

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, wird der HTTP-Aufruf im UTA-Flow-Baum angezeigt. Lassen Sie uns den Anruf zu finden ExternalPersonService.getPerson () und verspotten Sie es stattdessen:

Screenshot, der den Ablaufbaum des Unit Test Assistant von Parasoft Jtest zeigt

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();
        }
    }

Der Mockito mockStatic Die Methode erstellt ein statisches Modell für die Klasse, über das bestimmte statische Aufrufe konfiguriert werden können. Um sicherzustellen, dass sich dieser Test nicht auf andere im selben Lauf auswirkt, müssen MockedStatic-Objekte geschlossen werden, wenn der Test endet, sodass alle Mocks im geschlossen sind closeMocks() Methode, die der Testklasse hinzugefügt wird.

Mit UTA können wir nun die auswählen getPersonErgebnis variiere und instanziiere es, damit der verspottete Methodenaufruf nicht zurückkehrt null:

    String name="";//UTA:default value
    int age=0;//UTA:default value
    Person getPersonResult=newPerson(name, age);

Wenn wir den Test erneut ausführen, getPersonErgebnis wird von der verspotteten zurückgegebenExternalPersonService.getPerson () Methode und der Test besteht.

Hinweis: Im Flow-Baum können Sie auch "Mockable Method Pattern hinzufügen" für statische Methodenaufrufe auswählen. Dadurch wird Unit Unit Assistant so konfiguriert, dass diese statischen Methodenaufrufe beim Generieren neuer Tests immer verspottet werden.

Konklusion

Komplexe Anwendungen haben oft funktionale Abhängigkeiten, die die Fähigkeit eines Entwicklers, seinen Code zu testen, erschweren und einschränken. Die Verwendung eines Mocking-Frameworks wie Mockito kann Entwicklern dabei helfen, den zu testenden Code von diesen Abhängigkeiten zu isolieren, sodass sie schneller bessere Unit-Tests schreiben können. Parasofts Produktivitätslösung für Java-Entwickler Vereinfacht das Abhängigkeitsmanagement, indem neue Tests für die Verwendung von Mocks konfiguriert werden und fehlende Methoden-Mocks zur Laufzeit gefunden werden und Entwickler dabei unterstützt werden, Mocks für sie zu generieren.

Verbessern Sie Unit-Tests für Java mit Automatisierung: Best Practices für Java-Entwickler