Schreiben von Vorlagen für Testfälle mit JUnit 5

1. Übersicht

Die JUnit 5-Bibliothek bietet gegenüber ihren Vorgängerversionen viele neue Funktionen. Eine solche Funktion sind Testvorlagen. Kurz gesagt, Testvorlagen sind eine leistungsstarke Verallgemeinerung der parametrisierten und wiederholten Tests von JUnit 5.

In diesem Tutorial erfahren Sie, wie Sie mit JUnit 5 eine Testvorlage erstellen.

2. Maven-Abhängigkeiten

Beginnen wir mit dem Hinzufügen der Abhängigkeiten zu unserer pom.xml .

Wir müssen die Hauptabhängigkeit der JUnit 5- Junit-Jupiter-Engine hinzufügen :

 org.junit.jupiter junit-jupiter-engine 5.7.0 

Darüber hinaus müssen wir die Junit-Jupiter-API- Abhängigkeit hinzufügen :

 org.junit.jupiter junit-jupiter-api 5.7.0 

Ebenso können wir unserer build.gradle- Datei die erforderlichen Abhängigkeiten hinzufügen :

testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.0'

3. Die Problemstellung

Bevor wir uns die Testvorlagen ansehen, werfen wir einen kurzen Blick auf die parametrisierten Tests von JUnit 5. Parametrisierte Tests ermöglichen es uns, verschiedene Parameter in die Testmethode einzufügen. Wenn wir parametrisierte Tests verwenden, können wir daher eine einzelne Testmethode mehrmals mit unterschiedlichen Parametern ausführen.

Nehmen wir an, wir möchten unsere Testmethode jetzt mehrmals ausführen - nicht nur mit unterschiedlichen Parametern, sondern jedes Mal auch unter einem anderen Aufrufkontext.

Mit anderen Worten, wir möchten, dass die Testmethode mehrmals ausgeführt wird, wobei jeder Aufruf eine andere Kombination von Konfigurationen verwendet, wie z.

  • mit verschiedenen Parametern
  • Vorbereiten der Testklasseninstanz anders - dh Einfügen verschiedener Abhängigkeiten in die Testinstanz
  • Ausführen des Tests unter verschiedenen Bedingungen, z. B. Aktivieren / Deaktivieren einer Teilmenge von Aufrufen, wenn die Umgebung " QS " ist.
  • Ausführen mit einem anderen Lifecycle-Callback-Verhalten - Vielleicht möchten wir eine Datenbank vor und nach einer Teilmenge von Aufrufen einrichten und herunterfahren

Die Verwendung parametrisierter Tests erweist sich in diesem Fall schnell als begrenzt. Zum Glück bietet JUnit 5 eine leistungsstarke Lösung für dieses Szenario in Form von Testvorlagen.

4. Vorlagen testen

Testvorlagen selbst sind keine Testfälle. Stattdessen sind sie, wie der Name schon sagt, nur Vorlagen für bestimmte Testfälle. Sie sind eine leistungsstarke Verallgemeinerung von parametrisierten und wiederholten Tests.

Testvorlagen werden einmal für jeden Aufrufkontext aufgerufen, der ihnen von den Aufrufkontextanbietern bereitgestellt wird.

Schauen wir uns nun ein Beispiel für die Testvorlagen an. Wie wir oben festgestellt haben, sind die Hauptakteure:

  • eine Testzielmethode
  • eine Testvorlagenmethode
  • Ein oder mehrere Aufrufkontextanbieter, die bei der Vorlagenmethode registriert sind
  • einen oder mehrere Aufrufkontexte, die von jedem Aufrufkontextanbieter bereitgestellt werden

4.1. Die Testzielmethode

In diesem Beispiel verwenden wir eine einfache UserIdGeneratorImpl.generate- Methode als Testziel.

Definieren wir die UserIdGeneratorImpl- Klasse:

public class UserIdGeneratorImpl implements UserIdGenerator { private boolean isFeatureEnabled; public UserIdGeneratorImpl(boolean isFeatureEnabled) { this.isFeatureEnabled = isFeatureEnabled; } public String generate(String firstName, String lastName) { String initialAndLastName = firstName.substring(0, 1).concat(lastName); return isFeatureEnabled ? "bael".concat(initialAndLastName) : initialAndLastName; } }

Die generierte Methode, die unser Testziel ist, verwendet den Vornamen und den Nachnamen als Parameter und generiert eine Benutzer-ID. Das Format der Benutzer-ID hängt davon ab, ob ein Funktionsschalter aktiviert ist oder nicht.

Mal sehen, wie das aussieht:

Given feature switch is disabled When firstName = "John" and lastName = "Smith" Then "JSmith" is returned Given feature switch is enabled When firstName = "John" and lastName = "Smith" Then "baelJSmith" is returned

Als nächstes schreiben wir die Testvorlagenmethode.

4.2. Die Testvorlagenmethode

Hier ist eine Testvorlage für unsere Testzielmethode UserIdGeneratorImpl.generate :

public class UserIdGeneratorImplUnitTest { @TestTemplate @ExtendWith(UserIdGeneratorTestInvocationContextProvider.class) public void whenUserIdRequested_thenUserIdIsReturnedInCorrectFormat(UserIdGeneratorTestCase testCase) { UserIdGenerator userIdGenerator = new UserIdGeneratorImpl(testCase.isFeatureEnabled()); String actualUserId = userIdGenerator.generate(testCase.getFirstName(), testCase.getLastName()); assertThat(actualUserId).isEqualTo(testCase.getExpectedUserId()); } }

Schauen wir uns die Testvorlagenmethode genauer an.

Zunächst erstellen wir unsere Testvorlagenmethode, indem wir sie mit der Annotation JUnit 5 @TestTemplate markieren .

Im Anschluss daran registrieren wir einen Kontext - Provider , UserIdGeneratorTestInvocationContextProvider, mit der @ExtendWith Anmerkung . Wir können mehrere Kontextanbieter mit der Testvorlage registrieren. Für dieses Beispiel registrieren wir jedoch einen einzelnen Anbieter.

Außerdem empfängt die Template-Methode eine Instanz von UserIdGeneratorTestCase als Parameter. Dies ist einfach eine Wrapper-Klasse für die Eingaben und das erwartete Ergebnis des Testfalls:

public class UserIdGeneratorTestCase { private boolean isFeatureEnabled; private String firstName; private String lastName; private String expectedUserId; // Standard setters and getters }

Schließlich rufen wir die Testzielmethode auf und stellen fest, dass dieses Ergebnis wie erwartet ist

Jetzt ist es Zeit, unseren Aufrufkontextanbieter zu definieren .

4.3. Der Aufrufkontextanbieter

Wir müssen mindestens einen TestTemplateInvocationContextProvider mit unserer Testvorlage registrieren . Jeder registrierte TestTemplateInvocationContextProvider stellt einen Stream von TestTemplateInvocationContext- Instanzen bereit .

Previously, using the @ExtendWith annotation, we registered UserIdGeneratorTestInvocationContextProvider as our invocation provider.

Let's define this class now:

public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider { //... }

Our invocation context implements the TestTemplateInvocationContextProvider interface, which has two methods:

  • supportsTestTemplate
  • provideTestTemplateInvocationContexts

Let's start by implementing the supportsTestTemplate method:

@Override public boolean supportsTestTemplate(ExtensionContext extensionContext) { return true; }

The JUnit 5 execution engine calls the supportsTestTemplate method first to validate if the provider is applicable for the given ExecutionContext. In this case, we simply return true.

Now, let's implement the provideTestTemplateInvocationContexts method:

@Override public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { boolean featureDisabled = false; boolean featureEnabled = true; return Stream.of( featureDisabledContext( new UserIdGeneratorTestCase( "Given feature switch disabled When user name is John Smith Then generated userid is JSmith", featureDisabled, "John", "Smith", "JSmith")), featureEnabledContext( new UserIdGeneratorTestCase( "Given feature switch enabled When user name is John Smith Then generated userid is baelJSmith", featureEnabled, "John", "Smith", "baelJSmith")) ); }

The purpose of the provideTestTemplateInvocationContexts method is to provide a Stream of TestTemplateInvocationContext instances. For our example, it returns two instances, provided by the methods featureDisabledContext and featureEnabledContext. Consequently, our test template will run twice.

Next, let's look at the two TestTemplateInvocationContext instances returned by these methods.

4.4. The Invocation Context Instances

The invocation contexts are implementations of the TestTemplateInvocationContext interface and implement the following methods:

  • getDisplayName – provide a test display name
  • getAdditionalExtensions – return additional extensions for the invocation context

Let's define the featureDisabledContext method that returns our first invocation context instance:

private TestTemplateInvocationContext featureDisabledContext(   UserIdGeneratorTestCase userIdGeneratorTestCase) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return userIdGeneratorTestCase.getDisplayName(); } @Override public List getAdditionalExtensions() { return asList( new GenericTypedParameterResolver(userIdGeneratorTestCase), new BeforeTestExecutionCallback() { @Override public void beforeTestExecution(ExtensionContext extensionContext) { System.out.println("BeforeTestExecutionCallback:Disabled context"); } }, new AfterTestExecutionCallback() { @Override public void afterTestExecution(ExtensionContext extensionContext) { System.out.println("AfterTestExecutionCallback:Disabled context"); } } ); } }; }

Firstly, for the invocation context returned by the featureDisabledContext method, the extensions that we register are:

  • GenericTypedParameterResolver – a parameter resolver extension
  • BeforeTestExecutionCallback – a lifecycle callback extension that runs immediately before the test execution
  • AfterTestExecutionCallback – a lifecycle callback extension that runs immediately after the test execution

However, for the second invocation context, returned by the featureEnabledContext method, let's register a different set of extensions (keeping the GenericTypedParameterResolver):

private TestTemplateInvocationContext featureEnabledContext(   UserIdGeneratorTestCase userIdGeneratorTestCase) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return userIdGeneratorTestCase.getDisplayName(); } @Override public List getAdditionalExtensions() { return asList( new GenericTypedParameterResolver(userIdGeneratorTestCase), new DisabledOnQAEnvironmentExtension(), new BeforeEachCallback() { @Override public void beforeEach(ExtensionContext extensionContext) { System.out.println("BeforeEachCallback:Enabled context"); } }, new AfterEachCallback() { @Override public void afterEach(ExtensionContext extensionContext) { System.out.println("AfterEachCallback:Enabled context"); } } ); } }; }

For the second invocation context, the extensions that we register are:

  • GenericTypedParameterResolver – a parameter resolver extension
  • DisabledOnQAEnvironmentExtension – an execution condition to disable the test if the environment property (loaded from the application.properties file) is “qa
  • BeforeEachCallback – a lifecycle callback extension that runs before each test method execution
  • AfterEachCallback – a lifecycle callback extension that runs after each test method execution

From the above example, it is clear to see that:

  • the same test method is run under multiple invocation contexts
  • each invocation context uses its own set of extensions that differ both in number and nature from the extensions in other invocation contexts

As a result, a test method can be invoked multiple times under a completely different invocation context each time. And by registering multiple context providers, we can provide even more additional layers of invocation contexts under which to run the test.

5. Conclusion

In this article, we looked at how JUnit 5's test templates are a powerful generalization of parameterized and repeated tests.

Zunächst haben wir uns einige Einschränkungen der parametrisierten Tests angesehen. Als Nächstes haben wir erläutert, wie Testvorlagen die Einschränkungen überwinden, indem sie zulassen, dass ein Test für jeden Aufruf in einem anderen Kontext ausgeführt wird.

Schließlich haben wir uns ein Beispiel für die Erstellung einer neuen Testvorlage angesehen. Wir haben das Beispiel aufgeschlüsselt, um zu verstehen, wie Vorlagen in Verbindung mit den Anbietern von Aufrufkontexten und Aufrufkontexten funktionieren.

Wie immer ist der Quellcode für die in diesem Artikel verwendeten Beispiele auf GitHub verfügbar.