Java >> Java Tutorial >  >> Tag >> Spring

Komponententests von Spring MVC-Controllern:Konfiguration

Dieser Blogbeitrag ist veraltet! Wenn Sie lernen möchten, wie Sie Unit-Tests für Spring MVC-Controller schreiben können, sollten Sie sich mein aktualisiertes Spring MVC-Test-Tutorial ansehen. Es beschreibt, wie Sie Unit-Tests für Spring MVC-Controller mit JUnit 5 schreiben können.

Das Schreiben von Komponententests für Spring MVC-Controller war traditionell sowohl einfach als auch problematisch.

Obwohl es ziemlich einfach ist, Unit-Tests zu schreiben, die Controller-Methoden aufrufen, besteht das Problem darin, dass diese Unit-Tests nicht umfassend genug sind.

Beispielsweise können wir Controller-Mappings, Validierung und Ausnahmebehandlung nicht testen, indem wir einfach die getestete Controller-Methode aufrufen.

Spring MVC Test löste dieses Problem, indem es uns die Möglichkeit gab, Controller-Methoden über das DispatcherServlet aufzurufen .

Dies ist der erste Teil meines Tutorials, das die Unit-Tests von Spring MVC-Controllern beschreibt und beschreibt, wie wir unsere Unit-Tests konfigurieren können.

Fangen wir an.

Erforderliche Abhängigkeiten mit Maven erhalten

Wir können die erforderlichen Abhängigkeiten erhalten, indem wir die folgenden Testabhängigkeiten in unserer pom.xml deklarieren Datei:

  • JUnit 4.11
  • Mockito Core 1.9.5
  • Frühjahrstest 3.2.3.FREIGABE

Der relevante Teil unserer pom.xml Datei sieht wie folgt aus:

<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.11</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-core</artifactId>
	<version>1.9.5</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>3.2.3.RELEASE</version>
	<scope>test</scope>
</dependency>

Hinweis: Wenn Sie Spring Framework 3.1 verwenden müssen, können Sie Komponententests für Ihre Controller schreiben, indem Sie spring-test-mvc verwenden. Dieses Projekt wurde bei der Veröffentlichung von Spring Framework 3.2 in das Spring-Test-Modul aufgenommen.

Lassen Sie uns weitermachen und einen kurzen Blick auf unsere Beispielanwendung werfen.

Die Anatomie unserer Beispielanwendung

Die Beispielanwendung dieses Lernprogramms stellt CRUD-Operationen für Aufgabeneinträge bereit. Um die Konfiguration unserer Testklasse zu verstehen, müssen wir einige Kenntnisse über die getestete Controller-Klasse haben.

An diesem Punkt müssen wir die Antworten auf diese Fragen kennen:

  • Welche Abhängigkeiten hat es?
  • Wie wird es instanziiert?

Wir können die Antworten auf diese Fragen erhalten, indem wir einen Blick auf den Quellcode des TodoController werfen Klasse. Der relevante Teil des TodoController Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;

@Controller
public class TodoController {

    private final TodoService service;

    private final MessageSource messageSource;

    @Autowired
    public TodoController(MessageSource messageSource, TodoService service) {
        this.messageSource = messageSource;
        this.service = service;
    }

	//Other methods are omitted.
}

Wie wir sehen können, hat unsere Controller-Klasse zwei Abhängigkeiten:TodoService und MessageSource . Außerdem können wir sehen, dass unsere Controller-Klasse die Konstruktorinjektion verwendet.

An dieser Stelle sind das alle Informationen, die wir brauchen. Als nächstes werden wir über unsere Anwendungskontextkonfiguration sprechen.

Konfigurieren des Anwendungskontexts

Das Verwalten separater Anwendungskontextkonfigurationen für unsere Anwendung und unsere Tests ist umständlich. Außerdem kann es zu Problemen führen, wenn wir etwas in der Anwendungskontextkonfiguration unserer Anwendung ändern, aber vergessen, dieselbe Änderung für unseren Testkontext vorzunehmen.

Aus diesem Grund wurde die Anwendungskontextkonfiguration der Beispielanwendung so aufgeteilt, dass wir Teile davon in unseren Tests wiederverwenden können.

Unsere Anwendungskontextkonfiguration wurde wie folgt unterteilt:

  • Die erste Anwendungskonfigurationsklasse heißt ExampleApplicationContext und es ist die "Haupt"-Konfigurationsklasse unserer Anwendung.
  • Die zweite Konfigurationsklasse ist für die Konfiguration der Webschicht unserer Anwendung verantwortlich. Der Name dieser Klasse ist WebAppContext und es ist die Konfigurationsklasse, die wir in unseren Tests verwenden werden.
  • Die dritte Konfigurationsklasse heißt PersistenceContext und es enthält die Persistenzkonfiguration unserer Anwendung.

Hinweis: Die Beispielanwendung hat auch eine funktionierende Anwendungskontextkonfiguration, die XML-Konfigurationsdateien verwendet. Die XML-Konfigurationsdateien, die den Java-Konfigurationsklassen entsprechen, sind:exampleApplicationContext.xml , exampleApplicationContext-web.xml und exampleApplicationContext-persistence.xml .

Werfen wir einen Blick auf die Anwendungskontextkonfiguration unserer Webschicht und finden Sie heraus, wie wir unseren Testkontext konfigurieren können.

Konfigurieren der Webschicht

Die Anwendungskontextkonfiguration der Webschicht hat die folgenden Verantwortlichkeiten:

  1. Es aktiviert das annotationsgesteuerte Spring MVC.
  2. Es konfiguriert den Speicherort von statischen Ressourcen wie CSS-Dateien und Javascript-Dateien.
  3. Es stellt sicher, dass die statischen Ressourcen vom Standard-Servlet des Containers bereitgestellt werden.
  4. Es stellt sicher, dass die Controller-Klassen während des Komponenten-Scans gefunden werden.
  5. Es konfiguriert den ExceptionResolver Bohne.
  6. Es konfiguriert den ViewResolver Bohne.

Lassen Sie uns weitermachen und einen Blick auf die Java-Konfigurationsklasse und die XML-Konfigurationsdatei werfen.

Java-Konfiguration

Wenn wir die Java-Konfiguration verwenden, der Quellcode des WebAppContext Klasse sieht wie folgt aus:

mport org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

import java.util.Properties;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {
        "net.petrikainulainen.spring.testmvc.common.controller",
        "net.petrikainulainen.spring.testmvc.todo.controller"
})
public class WebAppContext extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("/static/");
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Bean
    public SimpleMappingExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();

        Properties exceptionMappings = new Properties();

        exceptionMappings.put("net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", "error/404");
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");

        exceptionResolver.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();

        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");

        exceptionResolver.setStatusCodes(statusCodes);

        return exceptionResolver;
    }

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();

        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");

        return viewResolver;
    }
}

XML-Konfiguration

Wenn wir die XML-Konfiguration verwenden, der Inhalt der exampleApplicationContext-web.xml Datei sieht wie folgt aus:

i<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <mvc:annotation-driven/>

    <mvc:resources mapping="/static/**" location="/static/"/>
    <mvc:default-servlet-handler/>

    <context:component-scan base-package="net.petrikainulainen.spring.testmvc.common.controller"/>
    <context:component-scan base-package="net.petrikainulainen.spring.testmvc.todo.controller"/>

    <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException">error/404</prop>
                <prop key="java.lang.Exception">error/error</prop>
                <prop key="java.lang.RuntimeException">error/error</prop>
            </props>
        </property>
        <property name="statusCodes">
            <props>
                <prop key="error/404">404</prop>
                <prop key="error/error">500</prop>
            </props>
        </property>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    </bean>
</beans>

Konfigurieren des Testkontexts

Die Konfiguration unseres Testkontextes hat zwei Verantwortlichkeiten:

  1. Es konfiguriert eine MessageSource Bean, die von unserer Controller-Klasse (Feedbackmeldungen) und Spring MVC (Validierungsfehlermeldungen) verwendet wird. Der Grund, warum wir dies tun müssen, ist, dass die MessageSource Bean wird in der "Haupt"-Konfigurationsklasse (oder -datei) unserer Anwendungskontextkonfiguration konfiguriert.
  2. Es erstellt einen TodoService mock, der in unsere Controller-Klasse eingefügt wird.

Lassen Sie uns herausfinden, wie wir unseren Testkontext mithilfe der Java-Konfigurationsklasse und der XML-Konfigurationsdatei konfigurieren.

Java-Konfiguration

Wenn wir unseren Testkontext mithilfe der Java-Konfiguration konfigurieren, wird der Quellcode des TestContext Klasse sieht wie folgt aus:

import org.mockito.Mockito;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;

@Configuration
public class TestContext {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);

        return messageSource;
    }

    @Bean
    public TodoService todoService() {
        return Mockito.mock(TodoService.class);
    }
}

XML-Konfiguration

Wenn wir unseren Testkontext mithilfe einer XML-Konfiguration konfigurieren, wird der Inhalt der testContext.xml Datei sieht wie folgt aus:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n/messages"/>
        <property name="useCodeAsDefaultMessage" value="true"/>
    </bean>

    <bean id="todoService" name="todoService" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg value="net.petrikainulainen.spring.testmvc.todo.service.TodoService"/>
    </bean>
</beans>

Konfigurieren der Testklasse

Wir können unsere Testklasse mit einer der folgenden Optionen konfigurieren:

  1. Die Standalone-Konfiguration ermöglicht es uns, einen oder mehrere Controller zu registrieren (Klassen, die mit @Controller kommentiert sind Anmerkung) und konfigurieren Sie die Spring MVC-Infrastruktur programmgesteuert. Dieser Ansatz ist eine praktikable Option, wenn unsere Spring MVC-Konfiguration einfach und unkompliziert ist.
  2. Der WebApplicationContext -basierte Konfiguration ermöglicht uns die Konfiguration der Spring MVC-Infrastruktur durch Verwendung eines vollständig initialisierten WebApplicationContext. Dieser Ansatz ist besser, wenn unsere Spring MVC-Konfiguration so kompliziert ist, dass die Verwendung einer eigenständigen Konfiguration keinen Sinn macht.

Lassen Sie uns weitermachen und herausfinden, wie wir unsere Testklasse konfigurieren können, indem wir beide Konfigurationsoptionen verwenden.

Standalone-Konfiguration verwenden

Wir können unsere Testklasse konfigurieren, indem wir diesen Schritten folgen:

  1. Kommentieren Sie die Klasse mit @RunWith Anmerkung und stellen Sie sicher, dass der Test mithilfe von MockitoJUnitRunner ausgeführt wird .
  2. Fügen Sie einen MockMvc hinzu Feld in die Testklasse.
  3. Fügen Sie einen TodoService hinzu -Feld in die Testklasse und kommentieren Sie das Feld mit @Mock Anmerkung. Diese Anmerkung markiert das Feld als Mock. Das Feld wird vom MockitoJUnitRunner initialisiert .
  4. Fügen Sie einen privaten ExceptionResolver() hinzu Methode zur Klasse. Diese Methode erstellt einen neuen SimpleMappingExceptionResolver Objekt, konfiguriert es und gibt das erstellte Objekt zurück.
  5. Fügen Sie eine private messageSource() hinzu Methode zur Klasse. Diese Methode erstellt eine neue ResourceBundleMessageSource Objekt, konfiguriert es und gibt das erstellte Objekt zurück.
  6. Fügen Sie einen privaten Validator() hinzu Methode zur Klasse. Diese Methode erstellt eine neue LocalValidatorFactoryBean Objekt und gibt das erstellte Objekt zurück.
  7. Fügen Sie einen privaten viewResolver() hinzu Methode zur Klasse. Diese Methode erstellt einen neuen InternalResourceViewResolver Objekt, konfiguriert es und gibt das erstellte Objekt zurück.
  8. Fügen Sie ein setUp() hinzu -Methode in die Testklasse und kommentieren Sie die Methode mit @Before Anmerkung. Dadurch wird sichergestellt, dass die Methode vor jedem Test aufgerufen wird. Diese Methode erstellt einen neuen MockMvc -Objekt durch Aufrufen von standaloneSetup() -Methode des MockMvcBuilders Klasse und konfiguriert die Spring MVC-Infrastruktur programmgesteuert.

Der Quellcode unserer Testklasse sieht wie folgt aus:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

import java.util.Properties;

@RunWith(MockitoJUnitRunner.class)
public class StandaloneTodoControllerTest {

    private MockMvc mockMvc;

    @Mock
    private TodoService todoServiceMock;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(new TodoController(messageSource(), todoServiceMock))
                .setHandlerExceptionResolvers(exceptionResolver())
                .setValidator(validator())
                .setViewResolvers(viewResolver())
                .build();
    }

    private HandlerExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();

        Properties exceptionMappings = new Properties();

        exceptionMappings.put("net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", "error/404");
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");

        exceptionResolver.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();

        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");

        exceptionResolver.setStatusCodes(statusCodes);

        return exceptionResolver;
    }

    private MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);

        return messageSource;
    }

    private LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }

    private ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();

        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");

        return viewResolver;
    }
}

Die Verwendung der Standalone-Konfiguration hat zwei Probleme:

  1. Unsere Testklasse sieht aus wie ein Durcheinander, obwohl unsere Spring MVC-Konfiguration ziemlich einfach ist. Natürlich könnten wir es bereinigen, indem wir die Erstellung von Spring MVC-Infrastrukturkomponenten in eine separate Klasse verschieben. Dies bleibt dem Leser als Übung überlassen.
  2. Wir müssen die Konfiguration der Spring MVC-Infrastrukturkomponenten duplizieren. Das heißt, wenn wir etwas in der Anwendungskontextkonfiguration unserer Anwendung ändern, müssen wir daran denken, die gleiche Änderung auch an unseren Tests vorzunehmen.

Kontextbasierte WebApplication-Konfiguration verwenden

Wir können unsere Testklasse konfigurieren, indem wir diesen Schritten folgen:

  1. Kommentieren Sie die Testklasse mit @RunWith Anmerkung und stellen Sie sicher, dass der Test mithilfe von SpringJUnit4ClassRunner ausgeführt wird .
  2. Annotieren Sie die Klasse mit @ContextConfiguration Anmerkung und stellen Sie sicher, dass die richtigen Konfigurationsklassen (oder XML-Konfigurationsdateien) verwendet werden. Wenn wir die Java-Konfiguration verwenden möchten, müssen wir die Konfigurationsklassen als Wert der Klassen festlegen Attribut. Wenn wir andererseits die XML-Konfiguration bevorzugen, müssen wir die Konfigurationsdateien als Wert der Orte festlegen Attribut.
  3. Annotieren Sie die Klasse mit @WebAppConfiguration Anmerkung. Diese Annotation stellt sicher, dass der Anwendungskontext, der für unseren Test geladen wird, ein WebApplicationContext ist .
  4. Fügen Sie einen MockMvc hinzu Feld in die Testklasse.
  5. Fügen Sie einen TodoService hinzu Feld zur Testklasse und kommentieren Sie das Feld mit @Autowired Anmerkung.
  6. Fügen Sie einen WebApplicationContext hinzu Feld zur Testklasse und kommentieren Sie das Feld mit @Autowired Anmerkung.
  7. Fügen Sie ein setUp() hinzu -Methode in die Testklasse und kommentieren Sie die Methode mit der @Before-Annotation. Dadurch wird sichergestellt, dass die Methode vor jedem Test aufgerufen wird. Diese Methode hat Aufgaben:Sie setzt den Service-Mock vor jedem Test zurück und erstellt einen neuen MockMvc -Objekt durch Aufrufen von webAppContextSetup() -Methode des MockMvcBuilders Klasse.

Der Quellcode unserer Testklasse sieht wie folgt aus:

import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
//@ContextConfiguration(locations = {"classpath:testContext.xml", "classpath:exampleApplicationContext-web.xml"})
@WebAppConfiguration
public class WebApplicationContextTodoControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private TodoService todoServiceMock;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void setUp() {
        //We have to reset our mock between tests because the mock objects
        //are managed by the Spring container. If we would not reset them,
        //stubbing and verified behavior would "leak" from one test to another.
        Mockito.reset(todoServiceMock);

        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }
}

Die Konfiguration unserer Testklasse sieht viel sauberer aus als die Konfiguration, die eine eigenständige Konfiguration verwendet. Der „Nachteil“ ist jedoch, dass unser Test die vollständige Spring MVC-Infrastruktur verwendet. Dies könnte ein Overkill sein, wenn unsere Testklasse wirklich nur wenige Komponenten verwendet.

Zusammenfassung

Wir haben jetzt unsere Einheitentestklasse konfiguriert, indem wir sowohl das Standalone-Setup als auch den WebApplicationContext verwendet haben basierte Einrichtung. Dieser Blogpost hat uns zwei Dinge gelehrt:

  • Wir haben gelernt, dass es wichtig ist, die Konfiguration des Anwendungskontexts so aufzuteilen, dass wir Teile davon in unseren Tests wiederverwenden können.
  • Wir haben den Unterschied zwischen der Standalone-Konfiguration und dem WebApplicationContext gelernt basierte Konfiguration.

Der nächste Teil dieses Tutorials beschreibt, wie wir Komponententests für "normale" Spring MVC-Controller schreiben können.

P.S. Sie können die Beispielanwendung dieses Blogbeitrags von Github herunterladen.


Java-Tag