Писане на шаблони за тестови случаи с помощта на JUnit 5

1. Общ преглед

Библиотеката JUnit 5 предлага много нови функции в сравнение с предишните си версии. Една такава функция са тестовите шаблони. Накратко, тестовите шаблони са мощно обобщение на параметризираните и повтарящи се тестове на JUnit 5.

В този урок ще научим как да създадем тестов шаблон с помощта на JUnit 5.

2. Зависимости на Maven

Нека започнем с добавяне на зависимостите към нашия pom.xml .

Трябва да добавим основната зависимост от двигател-юпитер-двигател JUnit 5 :

 org.junit.jupiter junit-jupiter-engine 5.7.0 

В допълнение към това ще трябва да добавим и зависимостта junit-jupiter-api :

 org.junit.jupiter junit-jupiter-api 5.7.0 

По същия начин можем да добавим необходимите зависимости към нашия файл build.gradle :

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. Посочването на проблема

Преди да разгледаме шаблони за тестове, нека да разгледаме накратко параметризираните тестове на JUnit 5. Параметризираните тестове ни позволяват да инжектираме различни параметри в тестовия метод. В резултат на това, когато използваме параметризирани тестове, можем да изпълним един метод за тестване няколко пъти с различни параметри.

Нека приемем, че сега бихме искали да стартираме нашия тестов метод няколко пъти - не само с различни параметри, но и под различен контекст на извикване всеки път.

С други думи, бихме искали тестовият метод да се изпълнява многократно, като всяко извикване използва различна комбинация от конфигурации като:

  • използвайки различни параметри
  • подготвяне на екземпляр на тестовия клас по различен начин - т.е. инжектиране на различни зависимости в тестовия екземпляр
  • провеждане на теста при различни условия, като активиране / деактивиране на подмножество извиквания, ако средата е „ QA
  • работи с различно поведение за обратно извикване на жизнения цикъл - може би искаме да настроим и разрушим база данни преди и след подмножество от извиквания

Използването на параметризирани тестове бързо се оказва ограничено в този случай. За щастие JUnit 5 предлага мощно решение за този сценарий под формата на тестови шаблони.

4. Тестови шаблони

Самите тестови шаблони не са тестови случаи. Вместо това, както подсказва името им, те са просто шаблони за дадени тестови случаи. Те са мощно обобщение на параметризирани и повтарящи се тестове.

Тестовите шаблони се извикват веднъж за всеки контекст на извикване, предоставен им от доставчика (ите) на контекста на извикване.

Нека сега разгледаме пример за тестовите шаблони. Както установихме по-горе, основните участници са:

  • метод за целева проба
  • метод на тестов шаблон
  • един или повече доставчици на контекст за извикване, регистрирани с метода на шаблона
  • един или повече контексти за извикване, предоставени от всеки доставчик на контекст за извикване

4.1. Методът на тестовата цел

В този пример ще използваме прост метод UserIdGeneratorImpl.generate като наша цел за тестване.

Нека дефинираме класа UserIdGeneratorImpl :

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

Методът за генериране , който е нашата цел на теста, взема firstName и lastName като параметри и генерира потребителски идентификатор. Форматът на идентификатора на потребителя варира в зависимост от това дали превключвателят на функции е активиран или не.

Нека да видим как изглежда това:

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

След това нека напишем метода на тестовия шаблон.

4.2. Методът на тестовия шаблон

Ето тестов шаблон за нашия тестов метод 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()); } }

Нека разгледаме по-отблизо метода на тестовия шаблон.

Като начало създаваме метода на тестовия ни шаблон, като го маркираме с анотацията JUnit 5 @TestTemplate .

След това регистрираме доставчик на контекст , UserIdGeneratorTestInvocationContextProvider, използвайки анотацията @ExtendWith . Можем да регистрираме множество доставчици на контекст с тестовия шаблон. За целите на този пример обаче регистрираме един доставчик.

Също така методът на шаблона получава екземпляр на UserIdGeneratorTestCase като параметър. Това е просто клас на обвивка за входовете и очаквания резултат от тестовия случай:

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

И накрая, извикваме метода на целевия тест и твърдим, че този резултат е както се очаква

Сега е време да дефинираме нашия доставчик на контекст за извикване .

4.3. Доставчикът на контекст за призоваване

Трябва да регистрираме поне един TestTemplateInvocationContextProvider с нашия тестов шаблон. Всеки регистриран TestTemplateInvocationContextProvider предоставя поток от екземпляри TestTemplateInvocationContext .

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.

Като начало разгледахме някои ограничения на параметризираните тестове. След това обсъдихме как тестовите шаблони преодоляват ограниченията, като позволяват да се изпълнява тест под различен контекст за всяко извикване.

Накрая разгледахме пример за създаване на нов тестов шаблон. Разбихме примера, за да разберем как работят шаблоните във връзка с доставчиците на контекст за извикване и контексти за извикване.

Както винаги, изходният код за примерите, използвани в тази статия, е достъпен в GitHub.