Тестируем и плачем Вместе со Spring Boot Test

@tolkv @lavcraft

@jekaborisov @jeka1978

В программе Тестирование живого приложения ● Старые подходы ○ ○ ○ ● Что нового нам приготовил Spring Boot? ○ ○ ○ ○ ○ ● ● @ContextConfiguration @ContextHierarchy && @DirtiesContext @ActiveProfiles @SpringBootTest @TestConfiguration @MockBean && @SpyBean && @*Beans @DataJpaTest @MvcTest Кэширование spring контекстов Шкала тестов

Немного теории

Unit Component Test Microservice Test System Test ➯ ➯ ➯ ➯ Шкала Тестов

Unit Component Test Microservice Test System Test ➯ ➯ ➯ ➯ Шкала Тестов

Unit/Component тесты. Для чего?

Unit/Component тесты. Для чего?

Unit/Component тесты. Для чего? Ваши тесты Тут

Unit/Component тесты. Для чего? Тесты уменьшают неопределённость

Есть два типа тестов Простой Сложный

Есть два типа тестов Какой сам выберешь Простой Сложный

Есть два типа тестов Какой сам выберешь, а какой разработчику оставишь? Простой Сложный

Когда пишут тесты?

Когда пишут тесты?

  1. Требование заказчика

Когда пишут тесты?

  1. Требование заказчика 2. Культура

Когда пишут тесты?

  1. Требование заказчика 2. Культура Перед кодом

Когда пишут тесты?

  1. Требование заказчика 2. Культура Перед кодом Вместе кодом

Когда пишут тесты?

  1. Требование заказчика 2. Культура Перед кодом Вместе кодом После кода

➯ ➯ Про какие тесты будем говорить? Перед кодом Unit Вместе кодом Component Test После кода

➯ ➯ Про какие тесты будем говорить? Перед кодом Unit Вместе кодом Component Test После кода

Начнём

Дано Default Answers Database router jbaruch-assistant Joker Resolver $tokens.joker JBaruch Resolver $tokens.jbaruch ... joker

Дано Чат поддержки тестировщиков Default Answers Database rest мы web assistant jbaruch-assistant res t joker-assistant rest Queue

Эксперты

Эксперты

Demo

Дано Чат поддержки тестировщиков Default Answers Database rest мы web assistant jbaruch-assistant res t joker-assistant rest Queue

А давайте тестировать

А давайте тестировать Default Answers Database router jbaruch-assistant Joker Resolver $tokens.joker JBaruch Resolver $tokens.jbaruch ... joker

Demo JokerWordsFrequencyResolverTest

А давайте тестировать. Тест #1 1. Пишем JokerWordsFrequencyResolverTest.

Demo

Кого тестируем @Component public class JokerWordsFrequencyResolver extends AbstractWordsFreqResolver { @Value("${tokens.joker}") private String answers; public JokerWordsFrequencyResolver(WordsComposer wordsComposer) { super(wordsComposer); } @Override public QuestionType getQuestionType() { return JOKER; } }

Тест №1.5 public class JokerWordsFrequencyResolverTest { @Test public void name() throws Exception { JokerWordsFrequencyResolver jokerWordsFrequencyResolver = new JokerWordsFrequencyResolver( ... ) ); jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Тест №1.5 public class JokerWordsFrequencyResolverTest { @Test public void name() throws Exception { JokerWordsFrequencyResolver jokerWordsFrequencyResolver = new JokerWordsFrequencyResolver( new WordsComposer( ... ) ); jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Тест №1.5 public class JokerWordsFrequencyResolverTest { @Test public void name() throws Exception { JokerWordsFrequencyResolver jokerWordsFrequencyResolver = new JokerWordsFrequencyResolver( new WordsComposer( new GarbageProperties() ) ); jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Тест №1 public class JokerWordsFrequencyResolverTest { @Test public void name() throws Exception { JokerWordsFrequencyResolver jokerWordsFrequencyResolver = new JokerWordsFrequencyResolver( new WordsComposer( new GarbageProperties() ) ); jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Тест №1 public class JokerWordsFrequencyResolverTest { @Test public void name() throws Exception { JokerWordsFrequencyResolver jokerWordsFrequencyResolver = new JokerWordsFrequencyResolver( new WordsComposer( new GarbageProperties() ) ); jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Результат java.lang.NullPointerException at … .(WordsComposer.java:48)

Not Passed

WordsComposer:48 garbageProperties.getGarbage() .contains(s.toLowerCase()) Nu ll Po in te rE xc ep ti on

WordsComposer:48 Запчасти Spring @Value("${garbage}") void setGarbage(String[] garbage) {

А давайте тестировать. Тест #1 1. 2. Пишем JokerWordsFrequencyResolverTest. Как ни крути, но нужен более “интеграционный тест”

➯ Шкала Тестов Unit

Unit Component Test ➯ ➯ Шкала Тестов

➯ ➯ Про какие тесты будем говорить? Перед кодом Unit Вместе кодом Component Test После кода

➯ ➯ Про какие тесты будем говорить? Перед кодом Unit Вместе кодом Component Test После кода

➯ ➯ Про какие тесты будем говорить? Инициализируется Spring`ом Unit Component Test Перед кодом Вместе кодом @Value("${garbage}") После кода void setGarbage(String[] garbage) {

Ещё немного теории

IoC, DI, Spring и друзья

IoC, DI, Spring и друзья

IoC, DI, Spring и друзья

IoC, DI, Spring и друзья Кино про супергероев

IoC, DI, Spring и друзья Кино про супергероев

IoC, DI, Spring и друзья Кино про супергероев

IoC, DI, Spring и друзья Кино про супергероев IoC

IoC, DI, Spring и друзья Кино про супергероев ФабрикаГероев IoC

IoC, DI, Spring и друзья IoC для инверсии поведения

IoC, DI, Spring и друзья IoC для инверсии поведения public class СуперЗлодейТест { Тоже инверсия контроля @Before public void setUp() throws Exception { ... } }

IoC, DI, Spring и друзья public class СъемочнаяПлощадка { public static void main(String[] args) { Киношка съёмка = new Киношка().снимать(); съёмка.герой.бить(); съёмка.злодей.страдать(); съёмка.злодей.бить(); съёмка.герой.страдать(); съёмка.герой.страдать(); } }

IoC, DI, Spring и друзья public class СъемочнаяПлощадка { public static void main(String[] args) { Киношка съёмка = new Киношка().снимать(); съёмка.герой.бить(); съёмка.злодей.страдать(); съёмка.злодей.бить(); съёмка.герой.страдать(); съёмка.герой.страдать(); } } NullPointerException

IoC, DI, Spring и друзья public class Киношка { СуперГерой герой; СуперЗлодей злодей; public Киношка снимать() { return new Киношка(); } }

IoC, DI, Spring и друзья public class СуперГерой implements Герой { private СуперЗлодей вражина; Кто проставляет? @Override public void бить() { вражина.бить(); } } public class СуперЗлодей implements Герой { private СуперГерой вражина; @Override public void бить() { вражина.страдать(); } }

IoC, DI, Spring и друзья public class ФабрикаГероев { public Object родить() { if (new Random().nextBoolean()) { return new СуперГерой(); } return new СуперЗлодей(); } }

IoC, DI, Spring и друзья public class ФабрикаГероев { public Object родить() { if (new Random().nextBoolean()) { return new СуперГерой(); } return new СуперЗлодей(); } }

IoC, DI, Spring и друзья Spring

IoC, DI, Spring и друзья ● ● ● Spring @Autowired @Component/@Service @Configuration

IoC, DI, Spring и друзья ● ● ● Spring @Autowired @Component/@Service @Configuration @Component public class Киношка { @Autowired СуперГерой герой; @Autowired СуперЗлодей злодей; public static Киношка снимать() { return new Киношка(); } }

IoC, DI, Spring и друзья ● ● ● Spring @Autowired @Component/@Service @Configuration @Component public class Киношка { @Autowired СуперГерой герой; @Autowired СуперЗлодей злодей; public static Киношка снимать() { return new Киношка(); } }

IoC, DI, Spring и друзья ● ● ● Spring @Autowired @Component/@Service @Configuration @Component public class СуперГерой implements Герой { @Autowired СуперЗлодей вражина; @Override public void бить() { вражина.бить(); } }

Demo

Тест №1.5 @RunWith(SpringRunner. class) @ContextConfiguration (classes = JokerWordsFrequencyResolverTestConfig. class) public class JokerWordsFrequencyResolverTest { @Autowired JokerWordsFrequencyResolver jokerWordsFrequencyResolver; @Test public void name() throws Exception { jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Тест №1.5 @Configuration public class JokerWordsFrequencyResolverTestConfig { @Bean public JokerWordsFrequencyResolver jokerWordsFrequencyResolver( WordsComposer wordsComposer) { return new JokerWordsFrequencyResolver(wordsComposer); } }

Тест №1.5 @Configuration public class JokerWordsFrequencyResolverTestConfig { @Bean public JokerWordsFrequencyResolver jokerWordsFrequencyResolver( WordsComposer wordsComposer) { return new JokerWordsFrequencyResolver(wordsComposer); } }

Тест №1.5 @Configuration @ComponentScan("com.conference.spring.test.common") public class JokerWordsFrequencyResolverTestConfig { @Bean public JokerWordsFrequencyResolver jokerWordsFrequencyResolver( WordsComposer wordsComposer) { return new JokerWordsFrequencyResolver(wordsComposer); } }

Тест №1.5 @Configuration @ComponentScan("com.conference.spring.test.common") public class JokerWordsFrequencyResolverTestConfig { @Bean public JokerWordsFrequencyResolver jokerWordsFrequencyResolver( WordsComposer wordsComposer) { return new JokerWordsFrequencyResolver(wordsComposer); } }

Тест №1.5 @RunWith(SpringRunner. class) @ContextConfiguration (classes = JokerWordsFrequencyResolverTestConfig. class) public class JokerWordsFrequencyResolverTest { @Autowired JokerWordsFrequencyResolver jokerWordsFrequencyResolver; @Test public void name() throws Exception { jokerWordsFrequencyResolver.setAnswers( "objects"); int match = jokerWordsFrequencyResolver.match( Question. builder().body("objects ...").build()); assertThat(match, equalTo(1)); } }

Passed

Ещё немного теории

SpringRunner /** * @author Sam Brannen * @since 4.3 * @see SpringJUnit4ClassRunner */ public final class SpringRunner extends SpringJUnit4ClassRunner

SpringRunner & SpringJUnit4ClassRunner /** * @author Sam Brannen * @author Juergen Hoeller * ... */ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner

SpringExtension — Junit5 /** * {@code SpringExtension} integrates the <em>Spring TestContext … </em> * into JUnit 5's <em>Jupiter</em> programming model. ... * @author Sam Brannen * @since 5.0 */ public class SpringExtension implements BeforeAllCallback, … {

SpringExtension — Junit5 @SpringJUnitConfig @SpringJUnitWebConfig

А давайте тестировать. Тест #2 1. Пишем TextBasedQuestionTypeResolverTest

Unit Component Test ➯ ➯ Шкала Тестов

А давайте тестировать. Тест #2 1. 2. Пишем TextBasedQuestionTypeResolverTest Вручную создаем три бина для тестирования TextBasedQuestionTypeResolver на примере Барух vs Джокер кейса

Demo TextBasedQuestionTypeResolverTest

Тест #2 @RunWith(SpringRunner. class) @ContextConfiguration (classes = TextBasedQuestionTypeResolverTestConfig. class) public class TextBasedQuestionTypeResolverTest { @Autowired TextBasedQuestionTypeResolver questionResolver; @Test public void name() throws Exception { QuestionType groovy = questionResolver.resolveType( new Question( "groovy")); QuestionType objects = questionResolver.resolveType( new Question("псих")); assertThat(groovy, equalTo(JBARUCH)); assertThat(objects, equalTo(JOKER)); } }

Тест #2 @Configuration public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } }

Тест #2 @Configuration public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } @Bean public JokerWordsFrequencyResolver … { … } @Bean public JBaruchWordsFrequencyResolver … { … } }

Тест #2 @Configuration public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } @Bean public JokerWordsFrequencyResolver … { … } @Bean public JBaruchWordsFrequencyResolver … { … } } Для них нужен WordsComposer @ComponentScan("com.conference.spring.test.common") ?

Тест #2 @Configuration public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } @Bean public JokerWordsFrequencyResolver … { … } @Bean public JBaruchWordsFrequencyResolver … { … } } Для них нужен WordsComposer @ComponentScan("com.conference.spring.test.common") ?

Тест #2 @Configuration @Import(CommonConfig. class) public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } @Bean public JokerWordsFrequencyResolver … { … } @Bean public JBaruchWordsFrequencyResolver … { … } }

Тест #2 @Configuration @ComponentScan("com.conference.spring.test.common") public class CommonConfig { }

Not Passed

Что случилось class JokerWordsFrequencyResolver @Value("${tokens.joker}") private String answers; Кто считывает? class JBaruchWordsFrequencyResolver @Value("${tokens.jbaruch}") private String answers;

Что случилось class JokerWordsFrequencyResolver @Value("${tokens.joker}") private String answers; Кто считывает? class JBaruchWordsFrequencyResolver @Value("${tokens.jbaruch}") private String answers; Отсюда application.yml: tokens: jbaruch: npm leftpad artifactory groovy object *** joker: objects считываем

Тест #2 @Configuration @Import(CommonConfig.class) @PropertySource("classpath*:application.yml") public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } @Bean public JokerWordsFrequencyResolver … { … } @Bean public JBaruchWordsFrequencyResolver … { … } }

Тест #2 @Configuration @Import(CommonConfig.class) @PropertySource("classpath*:application.yml") public class TextBasedQuestionTypeResolverTestConfig { @Bean public TextBasedQuestionTypeResolver textBasedQuestionTypeResolver( List<WordsFrequencyResolver> c) { return new TextBasedQuestionTypeResolver(c); } @Bean public JokerWordsFrequencyResolver … { … } @Bean public JBaruchWordsFrequencyResolver … { … } }

Not Passed

А давайте тестировать. Тест #2 1. 2. 3. 4. Пишем TextBasedQuestionTypeResolverTest Вручную создаем три бина для тестирования TextBasedQuestionTypeResolver на примере Барух vs Егор кейса Все падает потому что не подтягивается application.yml @PropertySource …

А давайте тестировать. Тест #2 @ContextConfiguration(classes = ....class, initializers = YamlFileApplicationContextInitializer.class) public class OurTest { @Test public test(){ ... } }

Spring Boot обновки 1. 2. 3. 4. 5. @SpringBootTest @TestConfiguration @MockBean && @SpyBean @DataJpaTest @MockMvcTest

Углубляемся в Spring. Тест #2 1. Применяем @SpringBootTest

Demo

Тест #2 @RunWith(SpringRunner. class) @ContextConfiguration (classes = TextBasedQuestionTypeResolverTestConfig. class) public class TextBasedQuestionTypeResolverTest { @Autowired TextBasedQuestionTypeResolver questionResolver; @Test public void name() throws Exception { QuestionType groovy = questionResolver.resolveType( new Question( "groovy")); QuestionType objects = questionResolver.resolveType( new Question("objects")); assertThat(groovy, equalTo(JBARUCH)); assertThat(objects, equalTo(JOKER)); } }

Тест #2 @RunWith(SpringRunner. class) @SpringBootTest public class TextBasedQuestionTypeResolverTest { @Autowired TextBasedQuestionTypeResolver questionResolver; @Test public void name() throws Exception { QuestionType groovy = questionResolver.resolveType( new Question( "groovy")); QuestionType objects = questionResolver.resolveType( new Question("objects")); assertThat(groovy, equalTo(JBARUCH)); assertThat(objects, equalTo(JOKER)); } }

Not Passed

Тест #2 @RunWith(SpringRunner. class) @SpringBootTest @ActiveProfiles ("joker_vs_jbaruch") application-joker_vs_jbaruch.yml public class TextBasedQuestionTypeResolverTest { @Autowired TextBasedQuestionTypeResolver questionResolver; Для подгрузки @Test public void name() throws Exception { QuestionType groovy = questionResolver.resolveType( new Question( "groovy")); QuestionType objects = questionResolver.resolveType( new Question("objects")); assertThat(groovy, equalTo(JBARUCH)); assertThat(objects, equalTo(JOKER)); } }

Passed

Углубляемся в Spring. Тест #2 1. Применяем @SpringBootTest 2. Долго… 3. @SpringBootTest(classes = ...class)

Углубляемся в Spring. Тест #2 1. 2. 3. 4. Применяем @SpringBootTest Долго… @SpringBootTest(classes = ...class) Стало быстрее

Demo - но можно лучше

Тест #2 @Configuration @ComponentScan("com.conference.spring.test.common") public class CommonConfig { @PostConstruct public void init() { System.out.println("Only once " + CommonConfig.class); } }

Запустим тест №1 и №2 за раз

Only once … only once … only once

Only once … only once … only once Дважды...

Углубляемся в Spring. Тест #2 1. 2. 3. 4. 5. Применяем @SpringBootTest Долго… @SpringBootTest(classes = ...class) Стало быстрее С кэшированием конфигураций – еще быстрее

Углубляемся в Spring. Тест #2 @ContextHierarchy({ @ContextConfiguration(classes=WordsCommonConfiguration.class), @ContextConfiguration(classes= ...class) })

Demo

@SpringBootTest @ContextHierarchy({ @ContextConfiguration(classes = TextBasedQuestionTypeResolverTestConfig.class), @ContextConfiguration(classes = CommonConfig.class) }) @ActiveProfiles("joker_vs_jbaruch") @RunWith(SpringRunner.class) public class TextBasedQuestionTypeResolverTest { ...

Запустим тест №1 и №2 за раз

Only once … only once … only once … only once … only once Четыре раза...

@Configuration @Import(CommonConfig.class) public class JokerWordsFrequencyResolverTestConfig { @Configuration @Import(CommonConfig.class) public class TextBasedQuestionTypeResolverTestConfig {

Убираем @Import(CommonConfig.class)

Not Passed

Не найден spring bean WordsComposer

Углубляемся в Spring. Тест #2 @ContextHierarchy({ @ContextConfiguration(classes=WordsCommonConfiguration.class), @ContextConfiguration(classes= ...class) }) Порядок важен! Т.к другая конфигурация использует бины из WordsCommonConfiguration

Меняем порядок в @ContextHierarchy @SpringBootTest CommonConfig теперь первый @ContextHierarchy({ @ContextConfiguration(classes = CommonConfig.class), @ContextConfiguration(classes = TextBasedQuestionTypeResolverTestConfig.class) }) @ActiveProfiles("joker_vs_jbaruch") @RunWith(SpringRunner.class) public class TextBasedQuestionTypeResolverTest { ...

Passed

Only once … only once … only once Дважды...

Сделали круг

Опять не закешировалось. Тест #2

Правила кэширования контекстов. Тест #2 @SpringBootTest – должен быть везде @Import – должен быть нигде @ActiveProfiles – один на всех SpringBootTest.properties – должны быть одинаковые

Правила кэширования контекстов. Тест #2 @SpringBootTest – должен быть везде @Import – должен быть нигде @ActiveProfiles – один на всех SpringBootTest.properties – должны быть одинаковые

Правила кэширования контекстов. Тест #2 @SpringBootTest – должен быть везде @Import – должен быть нигде @ActiveProfiles – один на всех SpringBootTest.properties – должны быть одинаковые Порядок важен! Любая перестановка – cache miss

Правила кэширования контекстов. Тест #2 @SpringBootTest(properties={"a=b","b=a"}) @SpringBootTest(properties={"b=a","a=b"})

Правила кэширования контекстов. Тест #2 @SpringBootTest(properties={"a=b","b=a"}) @SpringBootTest(properties={"b=a","a=b"}) Кэш не сработает

Правила кэширования контекстов. Тест #2 @SpringBootTest – должен быть везде @Import – должен быть нигде @ActiveProfiles – один на всех SpringBootTest.properties – должны быть одинаковые

Хрупкий кэш Все может привести к потере кэша

Пользуемся силой logging.level.org.springframework.test.context.cache=debug

Б – безопасность @SpringBootTest @ActiveProfiles("joker_vs_jbaruch") public abstract class ResolversAbstractCommonConfiguration { }

Only once … only once Один!...

А если наоборот? (как не кэшировать)

А если наоборот? (как не кэшировать) @DirtiesContext(...)

А если наоборот? (как не кэшировать) @DirtiesContext(...) methodMode() classMode() ... default MethodMode.AFTER_METHOD default ClassMode.AFTER_CLASS

Проверим наши знания. Тест #3 1. протестируем AnswerCacheServiceJPABackend

Demo AnswerCacheServiceJPABackend

Что тестируем @Service @RequiredArgsConstructor public class AnswerCacheServiceJPABackend implements AnswerCacheService { private final QuestionRepository questionRepository; private final AnswersRepository answersRepository; @Override public Answer find(Question question) { … } … }

Что тестируем @Service @RequiredArgsConstructor public class AnswerCacheServiceJPABackend implements AnswerCacheService { private final QuestionRepository questionRepository; private final AnswersRepository answersRepository; @Override public Answer find(Question question) { … } … }

Что тестируем @Service @RequiredArgsConstructor public class AnswerCacheServiceJPABackend implements AnswerCacheService { private final QuestionRepository questionRepository; private final AnswersRepository answersRepository; @Override public Answer find(Question question) { … } … }

Spring Boot обновки 1. 2. 3. 4. 5. @SpringBootTest @MockBean && @SpyBean @TestConfiguration @DataJpaTest @MockMvcTest

Как тестируем @RunWith(SpringRunner.class) @SpringBootTest(classes = AnswerCacheServiceJPABackendTestConfig.class) public class AnswerCacheServiceJPABackendTest { @Autowired AnswerCacheService answerCacheService; @MockBean AnswersRepository answersRepository; @MockBean QuestionRepository questionRepository; @Test public void should_not_fail() throws Exception { … test … } }

Как тестируем @RunWith(SpringRunner.class) @SpringBootTest(classes = AnswerCacheServiceJPABackendTestConfig.class) public class AnswerCacheServiceJPABackendTest { @Autowired AnswerCacheService answerCacheService; @MockBean AnswersRepository answersRepository; @MockBean QuestionRepository questionRepository; @Test public void should_not_fail() throws Exception { … test … } }

Как тестируем @RunWith(SpringRunner.class) @SpringBootTest(classes = AnswerCacheServiceJPABackendTestConfig.class) public class AnswerCacheServiceJPABackendTest { @Autowired AnswerCacheService answerCacheService; @MockBean AnswersRepository answersRepository; @MockBean QuestionRepository questionRepository; @Test public void should_not_fail() throws Exception { … test … } }

Как тестируем – Конфигурация @Configuration public class AnswerCacheServiceJPABackendTestConfig { @Bean public AnswerCacheServiceJPABackend answerCacheServiceJpaBackend( QuestionRepository qR, AnswersRepository aR) { return new AnswerCacheServiceJPABackend(qR, aR); } }

Как тестируем – сам тест @Test public void should_not_fail() throws Exception { Mockito.doThrow(new RuntimeException("Database is down")) .when(questionRepository) Наш @MockBean .findFirstByText(Matchers.anyString()); Answer answer = answerCacheService.find(Question.builder().build()); assertNull(answer); } }

Passed

Синергия с Mockito 1. @MockBean/@SpyBean 2. @PostConstruct для настройки 3. @Bean для настройки конкретных моков

Все ли хорошо? 1. Запустим все тесты

Not Passed

Все ли хорошо? 1. 2. Запустим все тесты DeveloperAssistantApplicationTests.contextLoad падает Стандартный тест на запуск контекст см start.spring.io

Почему упал Два бина одного типа в контексте ● ● answerCacheServiceJPABackend answerCacheServiceJpaBackend

Почему упал Два бина одного типа в контексте ● ● answerCacheServiceJPABackend answerCacheServiceJpaBackend

Как Spring называет бины? почему имена бинов разные

Все ли хорошо? 1. 2. 3. Запустим все тесты DeveloperAssistantApplicationTests.contextLoad падает Загрузил бины из другого теста!

Spring Boot обновки 1. 2. 3. 4. 5. @SpringBootTest @MockBean && @SpyBean @TestConfiguration @DataJpaTest @MockMvcTest

Все ли хорошо? 1. 2. 3. 4. Запустим все тесты DeveloperAssistantApplicationTests.contextLoad падает Загрузил бины из другого теста! @TestConfiguration!

@TestConfiguration 1. 2. 3. Не сканируется @SpringBootTest Не сканируется другими конфигурациями и тестами Не прерывает процесс сканирования @SpringBootTest

Все ли хорошо? 1. 2. 3. 4. 5. Запустим все тесты DeveloperAssistantApplicationTests.contextLoad падает Загрузил бины из другого теста! @TestConfiguration! DeveloperAssistantApplicationTests.contextLoad работает

Почему упал Два бина одного типа в контексте ● ● answerCacheServiceJPABackend answerCacheServiceJpaBackend

Почему упал Два бина одного типа в контексте ● ● answerCacheServiceJPABackend answerCacheServiceJpaBackend Опять двадцать пять!

Все ли хорошо? 1. 2. 3. 4. 5. 6. 7. Запустим все тесты DeveloperAssistantApplicationTests.contextLoad падает Загрузил бины из другого теста! @TestConfiguration! DeveloperAssistantApplicationTests.contextLoad работает А AnswerCacheServiceJPABackendTest перестал Загрузил бины из другого теста!

Spring Заговор

Как @SpringBootTest сканирует пакеты 1.

Два процесса сканирования 1. 2. @SpringBootTest сканирование @SpringBootApplication (@ComponentScan)

Два процесса сканирования 1. 2. @SpringBootTest сканирование @SpringBootApplication (@ComponentScan) Вверх

Два процесса сканирования 1. 2. @SpringBootTest сканирование @SpringBootApplication (@ComponentScan) Вверх Вниз

Два процесса сканирования @SpringBootTest

Два процесса сканирования @SpringBootTest

Два процесса сканирования @SpringBootTest

Два процесса сканирования @SpringBootTest

Два процесса сканирования @SpringBootTest test classpath extends main classpath

Два процесса сканирования @SpringBootTest test classpath extends main classpath src/main будет так же просканирован* @SpringBootApplication

Два процесса сканирования @SpringBootTest test classpath extends main classpath src/main будет так же просканирован* @SpringBootApplication

Два процесса сканирования @SpringBootTest test classpath extends main classpath src/main будет так же просканирован* @SpringBootApplication

Два процесса сканирования @SpringBootTest test classpath extends main classpath src/main будет так же просканирован* @SpringBootApplication

Два процесса сканирования @SpringBootTest test classpath extends main classpath src/main будет так же просканирован* @SpringBootApplication

Тоже и с src/main/**

Как чинить @SpringBootApplication @EnableFeignClients @EnableConfigurationProperties(AssistantProperties.class) public class DeveloperAssistantApplication { public static void main(String[] args) { SpringApplication.run(DeveloperAssistantApplication.class, args); } }

Как чинить @SpringBootApplication @EnableFeignClients @EnableConfigurationProperties(AssistantProperties.class) public class DeveloperAssistantApplication { public static void main(String[] args) { SpringApplication.run(DeveloperAssistantApplication.class, args); } }

Как чинить @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {

Как чинить @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {

Как чинить /** * @author Phillip Webb * @since 1.4.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { }

Все ли хорошо? 1. 2. 3. 4. 5. 6. 7. 8. Запустим все тесты DeveloperAssistantApplicationTests.contextLoad падает Загрузил бины из другого теста! @TestConfiguration! DeveloperAssistantApplicationTests.contextLoad работает А AnswerCacheServiceJPABackendTest перестал Загрузил бины из другого теста! @SpringBootConfiguration остановит сканирование

Чиним @SpringBootConfiguration public class StopConfiguration { } В нужном пакете!

Нужный пакет для остановки @SpringBootConfiguration

Component Tests

Spring Boot обновки 1. 2. 3. 4. 5. @SpringBootTest @TestConfiguration @MockBean && @SpyBean @DataJpaTest @MockMvcTest

@DataJpaTest 1. сканирует все репозитории

@DataJpaTest 1. 2. 3. сканирует все репозитории конфигурирует EntityManager загружает другие конфигурации

@DataJpaTest 1. 2. 3. 4. сканирует все репозитории конфигурирует EntityManager загружает другие конфигурации фильтрует все не относящееся к Data/JPA Применим знания

Тестируем DefaultAssistantJpaBackendTest 1. @DataJpaTest не загружает компоненты Spring*

Тестируем DefaultAssistantJpaBackendTest 1. 2. @DataJpaTest не загружает компоненты Spring* Делаем конфигурацию, загружаем недостающее

Тестируем DefaultAssistantJpaBackendTest 1. 2. 3. @DataJpaTest не загружает компоненты Spring Делаем конфигурацию, загружаем недостающее Ничего не работает, из за @SpringBootConfiguration

Тестируем DefaultAssistantJpaBackendTest 1. 2. 3. 4. @DataJpaTest не загружает компоненты Spring* Делаем конфигурацию, загружаем недостающее Ничего не работает, из за @SpringBootConfiguration Переносим в новый package – все @*Test тесты должны быть изолированы

@WebMvcTest 1. Не грузит компоненты спринга

@WebMvcTest 1. 2. Не грузит компоненты спринга Грузит только то что относится к Web

@WebMvcTest 1. 2. 3. Не грузит компоненты спринга Грузит только то что относится к Web Сразу изолируем в отдельный пакет Получаем суперспособность: @Autowired MockMvc mockMvc;

Где настраивать @MockBean 1. 2. В @*Configuration – если мок нужен на этапе создания контекста В тесте (@Before/setup/etc) если мок нужен только на этапе выполнения теста

Что же делает @SpringBootTest 1. Без classes a. b. 2. classes=~@Configuration a. 3. сканирует со своего пакета “вверх” в поисках @SpringBootConfiguration i. игнорирует остальных падает если не находит или находит несколько в одном пакете поднимет только указанные конфигурации classes=~@TestConfiguration a. поднимет указанный контекст и продолжит сканирование. см пункт 1

Зачем нужен @SpringBootTest 1. 2. 3. Полный тест на весь контекст Изменение properties Тесты с определенным скоупом – пакет/конфигурация/автоскан

Зачем нужен @TestConfiguration 1. 2. Если нужно не прерывать сканирование @SpringBootTest Изолированные тесты (игнорируется при сканировании)

Зачем нужен @SpringBootConfiguration 1. Прерывать сканирование инициированное @SpringBootTest

Есть два типа тестов Какой сам выберешь, а какой разработчику оставишь? Простой Сложный

Есть два типа тестов Какой сам выберешь, а какой разработчику оставишь? Простой Сложный Понятный

Выводы 1. Не боимся залезать в кишки приложения 2. Spring Boot богат на инструменты для тестирования 3. Но вносит свои ограничения – структура тестов

Unit Component Test Microservice Test System Test ➯ ➯ ➯ ➯ Шкала Тестов Следующий доклад

Unit Что нужно Junit/Mockito Кто управляет new Component Microservice @ContextConfiguration @SpringBootTest Spring Spring Boot

QA 229

Дополнительно 1. 2. 3. @ComponentScan > @TestConfiguration > @Configuratin ! @ComponentScan находит даже @TestConfiguration @DataJpaTest > @SpringBootTest @DataJpaTest и @WebMvcTest должны быть в отдельных пакетах Если есть сомнения – смотри автора! Juergen Hoeller*

Замечания 1. Spring для Unit тестирования может быть быстрым

Замечания 1. 2. Spring для Unit тестирования может быть быстрым Кэш контекстов – хрупкая штука

Замечания 1. 2. 3. Spring для Unit тестирования может быть быстрым Кэш контекстов – хрупкая штука Для тестов – только @TestConfiguration

Замечания 1. 2. 3. 4. Spring для Unit тестирования может быть быстрым Кэш контекстов – хрупкая штука Для тестов – только @TestConfiguration Изолировать группы тестов с помощью

Замечания 1. 2. 3. 4. Spring для Unit тестирования может быть быстрым Кэш контекстов – хрупкая штука Для тестов – только @TestConfiguration Изолировать группы тестов с помощью a. b. выделения в пакеты @SpringBootConfiguration

Замечания 1. 2. 3. 4. Spring для Unit тестирования может быть быстрым Кэш контекстов – хрупкая штука Для тестов – только @TestConfiguration Изолировать группы тестов с помощью a. b. 5. выделения в пакеты (особенно для @*Test) @SpringBootConfiguration SpringBootTest надо в основном использовать для микросервис тестов

Дополнительно 1. 2. 3. 4. Spring для Unit тестирования может быть быстрым Кэш контекстов – хрупкая штука Для тестов – только @TestConfiguration Изолировать группы тестов с помощью a. b. 5. 6. выделения в пакеты @SpringBootConfiguration SpringBootTest надо в основном использовать для микросервис тестов Если есть DirtiesContext – стоит задуматься :)

Ссылки 1. 2. 3. 4. 5. 6. Demo Source with Spring Boot 2.1 and Gradle — https://github.com/lavcraft/spring-boot-curse Old Demo Source with Spring Boot 1.5 and Maven — https://github.com/lavcraft/conference-test-with-spring-boot-test Spring Test Reference Guide Spring Boot Test Reference Guide Spring 1.4 Test Improvements Custom Test Slice with Spring Boot