JUnit, дай пять!
Про меня
• 9 лет работы в IT

• Project manager, analyst

• Java developer

• QA automation
dtuchs@gmail.com
+79217671422
https://linkedin.com/in/dtuchs/
JUnit 4?
• Java библиотека для написания и выполнения тестов

• Версия 4.0 выпущена в феврале 2006

• Java 1.5 - аннотации!

• Изоляция тестов путем создания class instance для
каждого теста

• Механизм расширений - Rule, ClassRule, custom
Runner
О проекте
• Личные кабинеты рекламодателя и веб-мастера

• 500+ функциональных тестов

• Java 8 + JUnit + Selenide + Maven + Allure, интеграция с
TestRail

• Внешние зависимости на другие сервисы, cоздание
preconditions через API, DB etc

• Идея - использовать готовые объекты
Почему не JUnit 4?
• Custom runner может быть только один, и тот занят

• Невозможно применять Rule к отдельным тестам

• Доступ к instance загруженного класса?

• DI не поддерживается ни для полей, ни для
параметров

• Последний релиз (4.12) был 4 года назад :(
Как выглядел тест с JUnit 4?
Спойлер: не очень
@Test
@TestCaseId(86509)
public void cpaTwoCountriesTwoRatesValidationTest() {
advertiser = CONTEXT.get()
.getSingle()
.getAdvertiser();
SspCountrySlices highSlice = COMMON_DB_SERVICE.getHighSliceForCountry(Country.US);
SspCountrySlices lowSlice = COMMON_DB_SERVICE.getHighSliceForCountry(Country.ES);
AdpApiRatesEntity highRate = new AdpApiRatesEntity()
.withAmount(highSlice.getHighestSlice().getBoundaryRejectedConversionPrice())
.withCountries(highSlice.getCountry().name());
AdpApiRatesEntity lowRate = new AdpApiRatesEntity()
.withAmount(lowSlice.getHighestSlice().getBoundaryRejectedConversionPrice())
.withCountries(lowSlice.getCountry().name());
cpaCampaign.setName(getRandomCPACampaignName())
.setRates(highRate, lowRate);
login(advertiser);
open(EditCampaignPage.ADD_PAGE_URL, EditCampaignPage.class)
.fillCpaRequiredFields(cpaCampaign)
.selectTargetingByDescription(highSlice.getHighestSlice().getPlatformName())
.tryToSaveCampaignAsDraft()
.checkCommonErrorMessageDisplayed(LOW_CONVERSION_PRICE_ERROR,
highSlice.getHighestSlice().getMinAcceptedConversionPrice());
}
Почему JUnit 5
@TestCaseId(86509)
@CsvSource({"US, IT"})
@ParameterizedTest
void cpaTwoCountriesTwoRatesValidationTest(@Rate AdpApiRatesEntity highRate,
@Rate AdpApiRatesEntity lowRate,
@SingleAdv SspAdvertiser advert,
@HighSlice(US) SspCountrySlices highSlice) {
cpaCampaign.setName(getRandomCPACampaignName())
.setRates(highRate, lowRate);
login(advert);
open(EditCampaignPage.ADD_PAGE_URL, EditCampaignPage.class)
.fillCpaRequiredFields(cpaCampaign)
.selectTargetingByDescription(highSlice.getHighestSlice().getPlatformName())
.tryToSaveCampaignAsDraft()
.checkCommonErrorMessageDisplayed(LOW_CONVERSION_PRICE_ERROR,
highSlice.getHighestSlice().getMinAcceptedConversionPrice());
}
Почему JUnit 5
• Единый механизм расширений, вместо Rule,
ClassRule, Runner

• Доступ к instance класса до запуска тестов

• DI для полей, для параметров конструктора и любых
методов

• Множество простых вариантов параметризации и
повторений тестов из коробки

• Проект активно развивается и поддерживается
• Extension model

• …
Extension Model. Callbacks
• Любой *Callback -
реализация интерфейса
Jupiter API с общей
сигнатурой:
void methodName(
ExtensionContext context)
• ExceptionHandler будет
вызван для обработки
Throwable, выброшенного из
теста.
Еще немного о Callback.
Мета-аннотации
• Заменим @ClassRule на BeforeAllCallback и
AfterAllCallback.
• Если @Rule вызывали base.evaluate() в try -
catch, то перенесем обработку в
TestExecutionExceptionHandler

• Callback на одном уровне будут выполнены в порядке
объявления: @ExtendWith({ExternalResourceSupport.class,
VerifierSupport.class})
• Аннотации могут наследовать
свойства
@Tag("fast")
@Test
public @interface FastTest
• Extension model

• Extension context

• …
ExtensionContext
• Несколько уровней вложенности. Контекст класса,
контекст метода

• Встроенный KV store с поддержкой пользовательских
namespace

• Если необходимо передавать объекты от одного
Extension к другому, используем ExtensionContext

• Простой доступ к аннотациям, полям и методам

• Доступ к исключению, не обработанному в
TestExecutionExceptionHandler
Поднимем драйвер, сделаем скрин
public class RemoteWebDriverRule extends TestWatcher implements TestRule {
@Override
protected void starting(Description description) {
initDriver(description.getMethodName());
}
@Override
protected void finished(Description description) {
finishDriver();
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
base.evaluate();
} catch (Throwable t) {
saveScreenShot("Screenshot on fail");
throw t;
}
}
};
}
}
Переписать на JUnit 5
будет сложно. Или нет?
Переписать на JUnit 5
будет сложно. Или нет?
public class RemoteWebDriverExtension implements BeforeEachCallback,
AfterEachCallback,
TestExecutionExceptionHandler {
@Override
public void beforeEach(ExtensionContext extensionContext) throws Exception {
Optional<Method> testMethod = extensionContext.getTestMethod();
initDriver(testMethod.isPresent() ? testMethod.get().getName() : null);
}
@Override
public void afterEach(ExtensionContext extensionContext) throws Exception {
finishDriver();
}
@Override
public void handleTestExecutionException(ExtensionContext extensionContext,
Throwable throwable) throws Throwable {
saveScreenShot("Screenshot on fail");
throw throwable;
}
}
• Extension model

• Extension context

• Execution condition

• …
ExecutionCondition
• Простой API, позволяющий проверить, прежде чем
запускать

• Любой Assume, выполнявшийся в первой строке теста
(или в методе @Before) может быть заменен на
ExecutionCondition
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(EnvironmentExtension.class)
public @interface Environment {
EnvType value();
enum EnvType {
STAGING, PRE_PRODUCTION, PRODUCTION
}
}
Demo
https://github.com/dtuchs/QAMeetupDemo
• Extension model

• Extension context

• Execution condition

• Parameter resolvers (DI)

• …
ParameterResolver (DI)
• Dependency injection - через параметры конструктора
или любого из методов lifecycle класса (@BeforeEach,
@Test и т. д.)

• ParameterResolver достаточно знать только тип
объекта (или его интерфейс)

• Расширяйте возможности, добавляя аннотации для
inject параметров
Demo
https://github.com/dtuchs/QAMeetupDemo
• Extension model

• Extension context

• Execution condition

• Parameter resolvers (DI)

• Parameter converters
Parameter Converter
• В отличие от Parameter Resolver - действительно
должен получать ссылку на какой-либо объект. 

• Уберите создание сложных сущностей из
DataProvider-ов. Пусть этим займется
ParameterConverter

• Не является имплементацией Extension, а значит - нет
доступа к ExtensionContext.

• Для использования желательно определить мета-
аннотацию, или использовать @ConvertWith(…)
Demo
https://github.com/dtuchs/QAMeetupDemo
• И еще много нового: asserts, nested tests,
dynamic tests, simple parameter sources…
Несколько НО
• Перезапуск теста, если что-то пошло не так. 

• Поддерживаемая версия Surefire - 2.19.1, Java - 8

• Многопоточное выполнение в Surefire пока не
поддерживается, но работает через fork

• Управление порядком запуска пока не
поддерживается

• Некоторые аннотации и интерфейсы находятся в
статусе Experimental API.
Вместо заключения
• Тесты стали лаконичнее

• Возможность построить свой, предметно-
ориентированный фрэймворк

• Мы не потеряли в стабильности и приобрели - в
скорости

• Мы верим, что выйдет Surefire 3.0
В тесте нет лишней
логики
static Stream<Arguments> archiveCampaignsSource() {
return Stream.of(
of(85044, "Archive CPM campaigns through action in table"),
of(86590, "Archive SmartCPA campaigns through action in table")
);
}
@DisplayName("85044, 86590: Archive CPM | Smart CPA campaigns through action in table")
@MethodSource("archiveCampaignsSource")
@ParameterizedTest
void archiveCampaignsThroughTableButtonTest(@Id Integer id,
@Name String name,
@SingleAdv SspAdvertiser advert,
@AllCampaigns List<SspCampaign> campaigns) {
login(advert);
open(CampaignsPage.PAGE_URL, CampaignsPage.class)
.tableActionPerform(ARCHIVE, campaigns)
.switchToArchivedTable()
.checkTableContainsCampaigns(campaigns);
}
Вопросы и ответы

JUnit, дай пять!

  • 1.
  • 2.
    Про меня • 9лет работы в IT • Project manager, analyst • Java developer • QA automation dtuchs@gmail.com +79217671422 https://linkedin.com/in/dtuchs/
  • 3.
    JUnit 4? • Javaбиблиотека для написания и выполнения тестов • Версия 4.0 выпущена в феврале 2006 • Java 1.5 - аннотации! • Изоляция тестов путем создания class instance для каждого теста • Механизм расширений - Rule, ClassRule, custom Runner
  • 4.
    О проекте • Личныекабинеты рекламодателя и веб-мастера • 500+ функциональных тестов • Java 8 + JUnit + Selenide + Maven + Allure, интеграция с TestRail • Внешние зависимости на другие сервисы, cоздание preconditions через API, DB etc • Идея - использовать готовые объекты
  • 5.
    Почему не JUnit4? • Custom runner может быть только один, и тот занят • Невозможно применять Rule к отдельным тестам • Доступ к instance загруженного класса? • DI не поддерживается ни для полей, ни для параметров • Последний релиз (4.12) был 4 года назад :(
  • 6.
    Как выглядел тестс JUnit 4? Спойлер: не очень @Test @TestCaseId(86509) public void cpaTwoCountriesTwoRatesValidationTest() { advertiser = CONTEXT.get() .getSingle() .getAdvertiser(); SspCountrySlices highSlice = COMMON_DB_SERVICE.getHighSliceForCountry(Country.US); SspCountrySlices lowSlice = COMMON_DB_SERVICE.getHighSliceForCountry(Country.ES); AdpApiRatesEntity highRate = new AdpApiRatesEntity() .withAmount(highSlice.getHighestSlice().getBoundaryRejectedConversionPrice()) .withCountries(highSlice.getCountry().name()); AdpApiRatesEntity lowRate = new AdpApiRatesEntity() .withAmount(lowSlice.getHighestSlice().getBoundaryRejectedConversionPrice()) .withCountries(lowSlice.getCountry().name()); cpaCampaign.setName(getRandomCPACampaignName()) .setRates(highRate, lowRate); login(advertiser); open(EditCampaignPage.ADD_PAGE_URL, EditCampaignPage.class) .fillCpaRequiredFields(cpaCampaign) .selectTargetingByDescription(highSlice.getHighestSlice().getPlatformName()) .tryToSaveCampaignAsDraft() .checkCommonErrorMessageDisplayed(LOW_CONVERSION_PRICE_ERROR, highSlice.getHighestSlice().getMinAcceptedConversionPrice()); }
  • 7.
    Почему JUnit 5 @TestCaseId(86509) @CsvSource({"US,IT"}) @ParameterizedTest void cpaTwoCountriesTwoRatesValidationTest(@Rate AdpApiRatesEntity highRate, @Rate AdpApiRatesEntity lowRate, @SingleAdv SspAdvertiser advert, @HighSlice(US) SspCountrySlices highSlice) { cpaCampaign.setName(getRandomCPACampaignName()) .setRates(highRate, lowRate); login(advert); open(EditCampaignPage.ADD_PAGE_URL, EditCampaignPage.class) .fillCpaRequiredFields(cpaCampaign) .selectTargetingByDescription(highSlice.getHighestSlice().getPlatformName()) .tryToSaveCampaignAsDraft() .checkCommonErrorMessageDisplayed(LOW_CONVERSION_PRICE_ERROR, highSlice.getHighestSlice().getMinAcceptedConversionPrice()); }
  • 8.
    Почему JUnit 5 •Единый механизм расширений, вместо Rule, ClassRule, Runner • Доступ к instance класса до запуска тестов • DI для полей, для параметров конструктора и любых методов • Множество простых вариантов параметризации и повторений тестов из коробки • Проект активно развивается и поддерживается
  • 9.
  • 10.
    Extension Model. Callbacks •Любой *Callback - реализация интерфейса Jupiter API с общей сигнатурой: void methodName( ExtensionContext context) • ExceptionHandler будет вызван для обработки Throwable, выброшенного из теста.
  • 11.
    Еще немного оCallback. Мета-аннотации • Заменим @ClassRule на BeforeAllCallback и AfterAllCallback. • Если @Rule вызывали base.evaluate() в try - catch, то перенесем обработку в TestExecutionExceptionHandler • Callback на одном уровне будут выполнены в порядке объявления: @ExtendWith({ExternalResourceSupport.class, VerifierSupport.class}) • Аннотации могут наследовать свойства @Tag("fast") @Test public @interface FastTest
  • 12.
    • Extension model •Extension context • …
  • 13.
    ExtensionContext • Несколько уровнейвложенности. Контекст класса, контекст метода • Встроенный KV store с поддержкой пользовательских namespace • Если необходимо передавать объекты от одного Extension к другому, используем ExtensionContext • Простой доступ к аннотациям, полям и методам • Доступ к исключению, не обработанному в TestExecutionExceptionHandler
  • 14.
    Поднимем драйвер, сделаемскрин public class RemoteWebDriverRule extends TestWatcher implements TestRule { @Override protected void starting(Description description) { initDriver(description.getMethodName()); } @Override protected void finished(Description description) { finishDriver(); } @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { base.evaluate(); } catch (Throwable t) { saveScreenShot("Screenshot on fail"); throw t; } } }; } }
  • 15.
    Переписать на JUnit5 будет сложно. Или нет?
  • 16.
    Переписать на JUnit5 будет сложно. Или нет? public class RemoteWebDriverExtension implements BeforeEachCallback, AfterEachCallback, TestExecutionExceptionHandler { @Override public void beforeEach(ExtensionContext extensionContext) throws Exception { Optional<Method> testMethod = extensionContext.getTestMethod(); initDriver(testMethod.isPresent() ? testMethod.get().getName() : null); } @Override public void afterEach(ExtensionContext extensionContext) throws Exception { finishDriver(); } @Override public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { saveScreenShot("Screenshot on fail"); throw throwable; } }
  • 17.
    • Extension model •Extension context • Execution condition • …
  • 18.
    ExecutionCondition • Простой API,позволяющий проверить, прежде чем запускать • Любой Assume, выполнявшийся в первой строке теста (или в методе @Before) может быть заменен на ExecutionCondition @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(EnvironmentExtension.class) public @interface Environment { EnvType value(); enum EnvType { STAGING, PRE_PRODUCTION, PRODUCTION } }
  • 19.
  • 20.
    • Extension model •Extension context • Execution condition • Parameter resolvers (DI) • …
  • 21.
    ParameterResolver (DI) • Dependencyinjection - через параметры конструктора или любого из методов lifecycle класса (@BeforeEach, @Test и т. д.) • ParameterResolver достаточно знать только тип объекта (или его интерфейс) • Расширяйте возможности, добавляя аннотации для inject параметров
  • 22.
  • 23.
    • Extension model •Extension context • Execution condition • Parameter resolvers (DI) • Parameter converters
  • 24.
    Parameter Converter • Вотличие от Parameter Resolver - действительно должен получать ссылку на какой-либо объект. • Уберите создание сложных сущностей из DataProvider-ов. Пусть этим займется ParameterConverter • Не является имплементацией Extension, а значит - нет доступа к ExtensionContext. • Для использования желательно определить мета- аннотацию, или использовать @ConvertWith(…)
  • 25.
  • 26.
    • И ещемного нового: asserts, nested tests, dynamic tests, simple parameter sources…
  • 27.
    Несколько НО • Перезапусктеста, если что-то пошло не так. • Поддерживаемая версия Surefire - 2.19.1, Java - 8 • Многопоточное выполнение в Surefire пока не поддерживается, но работает через fork • Управление порядком запуска пока не поддерживается • Некоторые аннотации и интерфейсы находятся в статусе Experimental API.
  • 28.
    Вместо заключения • Тестыстали лаконичнее • Возможность построить свой, предметно- ориентированный фрэймворк • Мы не потеряли в стабильности и приобрели - в скорости • Мы верим, что выйдет Surefire 3.0
  • 29.
    В тесте нетлишней логики static Stream<Arguments> archiveCampaignsSource() { return Stream.of( of(85044, "Archive CPM campaigns through action in table"), of(86590, "Archive SmartCPA campaigns through action in table") ); } @DisplayName("85044, 86590: Archive CPM | Smart CPA campaigns through action in table") @MethodSource("archiveCampaignsSource") @ParameterizedTest void archiveCampaignsThroughTableButtonTest(@Id Integer id, @Name String name, @SingleAdv SspAdvertiser advert, @AllCampaigns List<SspCampaign> campaigns) { login(advert); open(CampaignsPage.PAGE_URL, CampaignsPage.class) .tableActionPerform(ARCHIVE, campaigns) .switchToArchivedTable() .checkTableContainsCampaigns(campaigns); }
  • 30.