Предупреждение: я знаю, что JUnit для UNIT тестов и цена им небольшая. Речь о применении JUnit в ИНТЕГРАЦИОННЫХ тестах. И с другой стороны, на тех проектах, где мне приходилось работать, интеграционное тестирование проводилось вручную. Можно это дело автоматизировать.
Сами интеграционные тесты должны быть в отдельном проекте, не вместе с проектом/проектами. Что будет использовано в самих тестах решать самим (н.п. применить JUnit для тестирования REST, используя RestTemplate https://v.perm.ru/main/index.php/34-behave-testirovanie/ ).
Примеры тестирования https://javabydeveloper.com/junit-5-with-allure-reports-example/. В примере очень подробно описана работа с Allure, с различными группировками и прочими возможностями. Что будет протестировано, Rest или что-то другое, не важно.
Проведение тестов:
git clone https://github.com/javabydeveloper/Junit-5-tutorials-master/tree/master/junit5-allure-report-example cd junit5-allurreport-example mvn clean test allure:report }
Необходимое замечание об интеграционных тестах
JUnit для UNIT тестов и цена им небольшая. Речь о применении JUnit в ИНТЕГРАЦИОННЫХ тестах. На тех проектах, где мне приходилось работать, интеграционное тестирование проводилось вручную. Можно это дело автоматизировать. Нужно обеспечить начальное состояние теста и восстановить состояние после любого исхода теста. В самом простом случае можно установить его прямо в тесте. Или, лучше, воспользоваться средствами JUnit5:
- @BeforeAll - выполняется ПЕРЕД выполнением ВСЕХ методов класса теста
- @BeforeEach - выполняется ПЕРЕД выполнением КАЖДОГО метода класса теста
- @AfterEach - выполняется ПОСЛЕ выполнением КАЖДОГО метода класса теста
- @AfterAll - выполняется ПОСЛЕ выполнением ВСЕХ методов класса теста
О warning Allure
При генерации отчета может быть получен warning от allure типа "java.lang.NoSuchMethodError when run test with Junit 5...". Для решения нужно согласовать версию allure и JUnit. Использовать allure 2.10.0. Решение для gradle:
plugins { ... id 'io.qameta.allure' version '2.10.0' // version 2.10.0 WORK! NO WARNINGS for generate allure report ... } ... dependencies { ... testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter' ... }
Работающий пример: https://github.com/cherepakhin/kotlin_in_action
Наглядно и красиво!
Прогон конкретного теста:
./gradlew test --tests '*AssumptionsTest' ./gradlew test --tests ru.perm.v.project.AssumptionsTest ./gradlew test --tests 'ru.perm.v.project.MyTestClass*' ./gradlew test --tests '*Assumptions*'
Разные варианты тестирования:
./gradlew test --tests '*SomeTest.someSpecificFeature' ./gradlew test --tests '*SomeSpecificTest' ./gradlew test --tests '*IntegTest' ./gradlew test --tests '*IntegTest*ui*' ./gradlew test --tests '*IntegTest.singleMethod'
ВНИМАНИЕ на '*'. '*' - позволяет не указывать пакет.
Еще один момент - Gradle пропускает уже пройденные тесты, если код не изменился. В случае с тестирования внешнего сервися, сами тесты не меняются, а тестируюмая система может измениться. Нужно прогонять все тесты заново. Варианты решения:
./gradlew clean test ./gradlew cleanTest test ./gradlew test --rerun-tasks
Можно просто использовать "./gradlew test" для прогона ВСЕХ тестов, без учета предыдущих результатов. Для этого нужно настроить таск тестирования. На примере build.gradle.kts:
tasks.withType<Test> { .... outputs.upToDateWhen { false } // always rerun tests .... }
Пример https://github.com/cherepakhin/camel_rest_restassured_test/blob/main/build.gradle.kts/.
Пример тестирования с базой данных:
В проекте используется https://java.testcontainers.org/.
Проведение тестов :
./mvnw test
Еще один способ тестирования с заданием конфигурации бинов Spring boot (ПОДГЛЯДЕЛ в проекте https://github.com/saladlam/spring-wicket-noticeboard ):
package info.saladlam.example.spring.noticeboard.test; import info.saladlam.example.spring.noticeboard.support.Helper; import liquibase.integration.spring.SpringLiquibase; import org.apache.wicket.protocol.http.WicketFilter; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; import org.springframework.mock.web.MockFilterConfig; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.sql.DataSource; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) class LoginTest { @TestConfiguration public static class LoginTestContextConfiguration { // Определение бина для теста Spring Boot @Bean public DataSource testDataSource() { return Helper.getEmbeddedDatabaseBuilder(LoginTest.class.getName()).build(); } @Bean public SpringLiquibase liquibase(DataSource dataSource) { SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setChangeLog("classpath:db/user-test.xml"); liquibase.setDataSource(dataSource); liquibase.setDropFirst(true); return liquibase; } @Bean public Boolean finishWicketFilterInit(FilterRegistrationBean<WicketFilter> filter, ServletContext context) throws ServletException { MockFilterConfig config = new MockFilterConfig(context, "wicket-filter"); config.addInitParameter(WicketFilter.FILTER_MAPPING_PARAM, "/*"); WICKET_FILTER = filter.getFilter(); WICKET_FILTER.init(config); return true; } } private static WicketFilter WICKET_FILTER; @Autowired private MockMvc mockMvc; private Document getDocument(RequestBuilder requestBuilder) throws Exception { MvcResult result = mockMvc.perform(requestBuilder) .andExpect(status().isOk()) .andReturn(); return Jsoup.parse(result.getResponse().getContentAsString()); } private void sendLogin(MockHttpServletRequestBuilder requestBuilder, String username, String password, ResultMatcher matcher) throws Exception { mockMvc.perform(requestBuilder.with(csrf()) .param("username", username) .param("password", password) .contentType(MediaType.APPLICATION_FORM_URLENCODED)).andExpect(matcher); } @Test void loadLoginPage() throws Exception { Document doc = getDocument(get("/login")); assertThat(doc.select("input[name=username]")).hasSize(1); assertThat(doc.select("input[name=password]")).hasSize(1); } @Test void loginSuccess() throws Exception { sendLogin(post("/loginHandler"), "user1", "dIw8#a-$eW", redirectedUrl("/")); } @Test void loginFail() throws Exception { sendLogin(post("/loginHandler"), "user1", "password1", redirectedUrl("/login?error=true")); } @AfterAll static void tear() { WICKET_FILTER.destroy(); WICKET_FILTER = null; } }
Полезны параметризованные тесты (и не только в интеграционном тестировании):
https://www.baeldung.com/parameterized-tests-junit-5
Туториал по JUnit 5 - Аннотация @ParameterizedTest (Habr)
Пример:
@ParameterizedTest @ValueSource(ints = [1, 2]) fun getByWithParamN(N: Long) { val priceTypeService = PriceTypeServiceImpl(priceTypeRepository) assertEquals(N, priceTypeService.getByN(N).n) } @ParameterizedTest @CsvSource("1,'Normal price'", "2,'Discount price'") fun getByWithParamFromCasv(N: Long, NAME:String) { val priceTypeService = PriceTypeServiceImpl(priceTypeRepository) assertEquals(NAME, priceTypeService.getByN(N).name) }
Вид в Idea: