Статьи

Микросервисы для разработчиков Java: тестирование

1. Введение

С тех пор, как Кент Бек придумал идею разработки через тестирование ( TDD ) более десяти лет назад, тестирование стало неотъемлемой частью каждого программного проекта, нацеленного на успех. Прошли годы, сложность программных систем сильно возросла, так же как и методы тестирования, но те же основополагающие принципы все еще существуют и применяются.

Эффективное и действенное тестирование — это очень большая тема, полная мнений и окруженная бесконечными спорами о том, что можно и что нельзя делать . Многие думают о тестировании как об искусстве , и на то есть веские причины. В этой части руководства мы не собираемся присоединяться к каким-либо лагерям, а вместо этого сосредоточимся на тестировании приложений, которые реализованы в соответствии с принципами микросервисной архитектуры . Даже в такой узкой теме слишком много тем для обсуждения, поэтому следующие части учебника будут посвящены соответственно тестированию производительности и безопасности.

Но прежде чем мы начнем, пожалуйста, уделите некоторое время, чтобы ознакомиться со Стратегиями тестирования Марин Фаулер в микросервисной архитектуре , блестящим, подробным и хорошо иллюстрированным обзором подходов к управлению сложностью тестирования в мире микросервисов .

2. Модульное тестирование

Модульное тестирование является, вероятно, самой простой, но очень мощной формой тестирования, которая на самом деле не специфична для микросервисов, а для любого класса приложений или сервисов.

Юнит-тест выполняет наименьшую часть тестируемого программного обеспечения в приложении, чтобы определить, работает ли он должным образом. https://martinfowler.com/articles/microservice-testing/#testing-unit-introduction

Модульные тесты обычно должны составлять большую часть набора тестов приложений (согласно практической пирамиде тестов ), поскольку они должны быть очень простыми в написании и быстрыми в выполнении. В Java инфраструктура JUnit ( JUnit 4 и JUnit 5 ) является де-факто выбором в наши дни (хотя другие платформы, такие как TestNG или Spock , также широко используются).

Что может быть хорошим примером модульного теста ? Удивительно, но на этот вопрос очень сложно ответить, но есть несколько правил, которым нужно следовать: он должен тестировать один конкретный компонент («единицу») изолированно, он должен тестировать одну вещь за раз, и она должна быть быстрой.

Существует множество модульных тестов, которые входят в комплект сервисных тестов платформы JCG Car Rentals . Давайте выберем Службу поддержки клиентов и рассмотрим фрагмент набора тестов для класса AddressToAddressEntityConverter , который преобразует объект передачи данных Address в соответствующую персистентную сущность JPA .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AddressToAddressEntityConverterTest {
    private AddressToAddressEntityConverter converter;
     
    @Before
    public void setUp() {
        converter = new AddressToAddressEntityConverter();
    }
     
    @Test
    public void testConvertingNullValueShouldReturnNull() {
        assertThat(converter.convert(null)).isNull();
    }
     
    @Test
    public void testConvertingAddressShouldSetAllNonNullFields() {
        final UUID uuid = UUID.randomUUID();
         
        final Address address = new Address(uuid)
            .withStreetLine1("7393 Plymouth Lane")
            .withPostalCode("19064")
            .withCity("Springfield")
            .withStateOrProvince("PA")
            .withCountry("United States of America");
         
        assertThat(converter.convert(address))
            .isNotNull()
            .hasFieldOrPropertyWithValue("uuid", uuid)
            .hasFieldOrPropertyWithValue("streetLine1", "7393 Plymouth Lane")
            .hasFieldOrPropertyWithValue("streetLine2", null)
            .hasFieldOrPropertyWithValue("postalCode", "19064")
            .hasFieldOrPropertyWithValue("city", "Springfield")
            .hasFieldOrPropertyWithValue("stateOrProvince", "PA")
            .hasFieldOrPropertyWithValue("country", "United States of America");
    }
}

Тест довольно прост, его легко прочитать, понять и устранить любые сбои, которые могут возникнуть в будущем. В реальных проектах модульные тесты могут очень быстро выйти из-под контроля, стать раздутыми и их трудно поддерживать. Универсального лечения такого заболевания не существует, но общий совет — рассматривать тестовые случаи в качестве основного кода.

3. Интеграционное тестирование

В действительности, компоненты (или «блоки») в наших приложениях часто зависят от других компонентов, хранилищ данных, внешних служб, кэшей, брокеров сообщений… Так как модульные тесты фокусируются на изоляции, нам нужно подняться на один уровень вверх и переключиться на интеграционное тестирование .

Интеграционный тест проверяет пути связи и взаимодействия между компонентами для выявления дефектов интерфейса. https://martinfowler.com/articles/microservice-testing/#testing-integration-introduction

Вероятно, лучшим примером для демонстрации возможностей интеграционного тестирования является создание пакета для тестирования персистентного уровня. Это та область, в которой такие среды , как Arquillian , Mockito , DBUnit , Wiremock , Testcontainers , REST Assured (и многие другие), играют ведущую роль.

Давайте вернемся в Службу поддержки клиентов и подумаем, как обеспечить, чтобы данные клиентов действительно сохранялись в базе данных. У нас есть специальный регистрационный сервис для управления процессом регистрации, поэтому нам нужно предоставить экземпляр базы данных, связать все зависимости и начать процесс регистрации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@RunWith(Arquillian.class)
public class TransactionalRegistrationServiceIT {
    @Inject private RegistrationService service;
         
    @Deployment
    public static JavaArchive createArchive() {
        return ShrinkWrap
            .create(JavaArchive.class)
            .addClasses(CustomerJpaRepository.class, PersistenceConfig.class)
            .addClasses(ConversionService.class, TransactionalRegistrationService.class)
            .addPackages(true, "org.apache.deltaspike")
            .addPackages(true, "com.javacodegeeks.rentals.customer.conversion")
            .addPackages(true, "com.javacodegeeks.rentals.customer.registration.conversion");
    }
     
    @Test
    public void testRegisterNewCustomer() {
        final RegisterAddress homeAddress = new RegisterAddress()
            .withStreetLine1("7393 Plymouth Lane")
            .withPostalCode("19064")
            .withCity("Springfield")
            .withCountry("United States of America")
            .withStateOrProvince("PA");
         
        final RegisterCustomer registerCustomer = new RegisterCustomer()
            .withFirstName("John")
            .withLastName("Smith")
            .withEmail("john@smith.com")
            .withHomeAddress(homeAddress);
         
        final UUID uuid = UUID.randomUUID();
        final Customer customer = service.register(uuid, registerCustomer);
         
        assertThat(customer).isNotNull()
            .satisfies(c -> {
                assertThat(c.getUuid()).isEqualTo(uuid);
                assertThat(c.getFirstName()).isEqualTo("John");
                assertThat(c.getLastName()).isEqualTo("Smith");
                assertThat(c.getEmail()).isEqualTo("john@smith.com");
                assertThat(c.getBillingAddress()).isNull();
                assertThat(customer.getHomeAddress()).isNotNull()
                    .satisfies(a -> {
                        assertThat(a.getUuid()).isNotNull();
                        assertThat(a.getStreetLine1()).isEqualTo("7393 Plymouth Lane");
                        assertThat(a.getStreetLine2()).isNull();
                        assertThat(a.getCity()).isEqualTo("Springfield");
                        assertThat(a.getPostalCode()).isEqualTo("19064");
                        assertThat(a.getStateOrProvince()).isEqualTo("PA");
                        assertThat(a.getCountry()).isEqualTo("United States of America");
                    });
            });
    }
}

Это набор тестов на основе Arquillian, в котором мы настроили ядро базы данных H2 в памяти в режиме совместимости с PostgreSQL (через файл свойств). Даже в этой конфигурации может потребоваться до 15-25 секунд, что намного быстрее, чем вращение выделенного экземпляра базы данных PostgreSQL .

Время выполнения торговых интеграционных тестов путем замены компонентов интеграции — один из жизнеспособных методов для более быстрого получения обратной связи. Конечно, это может не сработать для всех и каждого, поэтому мы вернемся к этой теме позже в этой части руководства.

Если ваши микросервисы построены на основе Spring Framework и Spring Boot , как, например, наша служба резервирования , вы определенно выиграете от автоматически настроенных тестовых срезов и насмешек bean-компонентов . Приведенный ниже фрагмент, @WebFluxTest частью @WebFluxTest тестов ReservationController , иллюстрирует использование тестового фрагмента @WebFluxTest в действии.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@WebFluxTest(ReservationController.class)
class ReservationControllerTest {
    private final String username = "b36dbc74-1498-49bd-adec-0b53c2b268f8";
     
    private final UUID customerId = UUID.fromString(username);
    private final UUID vehicleId = UUID.fromString("397a3c5c-5c7b-4652-a11a-f30e8a522bf6");
    private final UUID reservationId = UUID.fromString("3f8bc729-253d-4d8f-bff2-bc07e1a93af6");
     
    @Autowired
    private WebTestClient webClient;
    @MockBean
    private ReservationService service;
    @MockBean
    private InventoryServiceClient inventoryServiceClient;
 
    @Test
    @DisplayName("Should create Customer reservation")
    @WithMockUser(roles = "CUSTOMER", username = username)
    public void shouldCreateCustomerReservation() {
        final OffsetDateTime reserveFrom = OffsetDateTime.now().plusDays(1);
        final OffsetDateTime reserveTo = reserveFrom.plusDays(2);
 
        when(inventoryServiceClient.availability(eq(vehicleId)))
            .thenReturn(Mono.just(new Availability(vehicleId, true)));
         
        when(service.reserve(eq(customerId), any()))
            .thenReturn(Mono.just(new Reservation(reservationId)));
         
        webClient
            .mutateWith(csrf())
            .post()
            .uri("/api/reservations")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .body(BodyInserters
                .fromObject(new CreateReservation()
                    .withVehicleId(vehicleId)
                    .withFrom(reserveFrom)
                    .withTo(reserveTo)))
            .exchange()
            .expectStatus().isCreated()
            .expectBody(Reservation.class)
            .value(r -> {
                assertThat(r)
                    .extracting(Reservation::getId)
                    .isEqualTo(reservationId);
            });
    }
}

Честно говоря, удивительно видеть, сколько усилий и мыслей команда Spring вкладывает в поддержку тестирования. Мало того, что мы можем покрыть большую часть обработки запросов и ответов, не вращая экземпляр сервера, время выполнения теста невероятно быстро.

Еще одна интересная концепция, с которой вы часто сталкиваетесь, особенно при интеграционном тестировании , — это использование подделок , заглушек , тестовых двойников и / или макетов .

4. Тестирование асинхронных потоков

Весьма вероятно, что рано или поздно вы столкнетесь с необходимостью протестировать какую-либо функциональность, основанную на асинхронной обработке. Если честно, без использования выделенных диспетчеров или исполнителей это действительно сложно из-за недетерминированной природы потока выполнения.

Если мы немного вернемся к моменту, когда мы обсуждали реализацию микросервисов , мы столкнулись бы с потоком в Службе поддержки клиентов, который опирается на асинхронное распространение событий, предоставляемое CDI 2.0 . Как бы мы это проверили? Давайте выясним один из возможных способов решения этой проблемы, рассмотрев фрагмент ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RunWith(Arquillian.class)
public class NotificationServiceTest {
    @Inject private RegistrationService registrationService;
    @Inject private TestNotificationService notificationService;
         
    @Deployment
    public static JavaArchive createArchive() {
        return ShrinkWrap
            .create(JavaArchive.class)
            .addClasses(TestNotificationService.class, StubCustomerRepository.class)
            .addClasses(ConversionService.class, TransactionalRegistrationService.class, RegistrationEventObserver.class)
            .addPackages(true, "org.apache.deltaspike.core")
            .addPackages(true, "com.javacodegeeks.rentals.customer.conversion")
            .addPackages(true, "com.javacodegeeks.rentals.customer.registration.conversion");
    }
     
    @Test
    public void testCustomerRegistrationEventIsFired() {
        final UUID uuid = UUID.randomUUID();
        final Customer customer = registrationService.register(uuid, new RegisterCustomer());
         
        await()
            .atMost(1, TimeUnit.SECONDS)
            .until(() -> !notificationService.getTemplates().isEmpty());
         
        assertThat(notificationService.getTemplates())
            .hasSize(1)
            .hasOnlyElementsOfType(RegistrationTemplate.class)
            .extracting("customerId")
            .containsOnly(customer.getUuid());
    }
}

Поскольку событие инициируется и потребляется асинхронно , мы не можем делать утверждения предсказуемо, но принимаем во внимание аспект синхронизации, используя библиотеку Awaitility . Кроме того, нам не нужно задействовать постоянный уровень в этом наборе тестов, поэтому мы предоставляем собственную (довольно StubCustomerRepository чтобы быть справедливой) реализацию StubCustomerRepository для ускорения выполнения теста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Singleton
public static class StubCustomerRepository implements CustomerRepository {
    @Override
    public Optional<CustomerEntity> findById(UUID uuid) {
        return Optional.empty();
    }
 
    @Override
    public CustomerEntity saveOrUpdate(CustomerEntity entity) {
        return entity;
    }
 
    @Override
    public boolean deleteById(UUID uuid) {
        return false;
    }
}

Тем не менее, даже при таком подходе есть возможности для нестабильности. Выделенные диспетчеры и исполнители тестов могут давать лучшие результаты, но не каждая среда предоставляет их или поддерживает простые способы их подключения.

5. Тестирование запланированных задач

Работа, которая должна быть выполнена (или запланирована) в определенное время, представляет собой интересную проблему с точки зрения тестирования. Как бы мы убедились, что график соответствует ожиданиям? Было бы непрактично (но, верьте или нет, реалистично) иметь тестовые наборы, работающие часами или днями в ожидании запуска задачи. К счастью, есть несколько вариантов для рассмотрения. Для приложений и сервисов, которые используют Spring Framework, самый простой, но достаточно надежный маршрут — это использовать CronTrigger и макет (или заглушку) TriggerContext , например.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SchedulingTest {
    private final class TestTriggerContext implements TriggerContext {
        private final Date lastExecutionTime;
 
        private TestTriggerContext(LocalDateTime lastExecutionTime) {
            this.lastExecutionTime =  Date.from(lastExecutionTime.atZone(ZoneId.systemDefault()).toInstant());
        }
 
        @Override
        public Date lastScheduledExecutionTime() {
            return lastExecutionTime;
        }
 
        @Override
        public Date lastActualExecutionTime() {
            return lastExecutionTime;
        }
 
        @Override
        public Date lastCompletionTime() {
            return lastExecutionTime;
        }
    }
 
    @Test
    public void testScheduling(){
        final CronTrigger trigger = new CronTrigger("0 */30 * * * *");
 
        final LocalDateTime lastExecutionTime = LocalDateTime.of(2019, 01, 01, 10, 00, 00);
        final Date nextExecutionTime = trigger.nextExecutionTime(new TestTriggerContext(lastExecutionTime));
         
        assertThat(nextExecutionTime)
            .hasYear(2019)
            .hasMonth(01)
            .hasDayOfMonth(01)
            .hasHourOfDay(10)
            .hasMinute(30)
            .hasSecond(0);
    }
}

Приведенный выше тестовый пример использует фиксированное выражение CronTrigger и проверяет время следующего выполнения, но оно также может быть заполнено из свойств или даже аннотаций метода класса.

В качестве альтернативы проверке самого графика, вы можете найти очень полезным полагаться на виртуальные часы и буквально «путешествовать во времени». Например, вы можете передать экземпляр абстрактного класса Clock (часть стандартной библиотеки Java ) и заменить его в тестах заглушкой или макетом.

6. Тестирование реактивных потоков

Популярность парадигмы реактивного программирования оказала глубокое влияние на используемые нами методы тестирования. На самом деле, поддержка тестирования — это первоклассный гражданин в любой реактивной среде : RxJava , Project Reactor или Akka Streams , как вы это называете.

Наша служба бронирования полностью построена с использованием стека Spring Reactive и является отличным кандидатом для иллюстрации использования выделенных скаффолдингов для тестирования реагирующих API .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Testcontainers
@SpringBootTest(
    classes = ReservationRepositoryIT.Config.class,
    webEnvironment = WebEnvironment.NONE
)
public class ReservationRepositoryIT {
    @Container
    private static final GenericContainer<?> container = new GenericContainer<>("cassandra:3.11.3")
        .withTmpFs(Collections.singletonMap("/var/lib/cassandra", "rw"))
        .withExposedPorts(9042)
        .withStartupTimeout(Duration.ofMinutes(2));
     
    @Configuration
    @EnableReactiveCassandraRepositories
    @ImportAutoConfiguration(CassandraMigrationAutoConfiguration.class)
    @Import(CassandraConfiguration.class)
    static class Config {
    }
 
    @Autowired
    private ReservationRepository repository;
     
    @Test
    @DisplayName("Should insert Customer reservations")
    public void shouldInsertCustomerReservations() {
        final UUID customerId = randomUUID();
 
        final Flux<ReservationEntity> reservations =
            repository
                .deleteAll()
                .thenMany(
                    repository.saveAll(
                        Flux.just(
                            new ReservationEntity(randomUUID(), randomUUID())
                                .withCustomerId(customerId),
                            new ReservationEntity(randomUUID(), randomUUID())
                                .withCustomerId(customerId))));
 
        StepVerifier
            .create(reservations)
            .expectNextCount(2)
            .verifyComplete();
    }
}

Помимо использования поддержки тестирования Spring Boot , этот набор тестов опирается на выдающиеся возможности тестирования Spring Reactor в форме StepVerifier где ожидания определяются в терминах событий, ожидаемых на каждом шаге. Функциональность, которую предоставляют StepVerifier и семейство, вполне достаточна для покрытия произвольных сложных сценариев.

Еще одна вещь, о которой следует упомянуть, это использование инфраструктуры Testcontainers и начальная загрузка выделенного экземпляра хранилища данных (в данном случае Apache Cassandra ) для сохранения. При этом тестируются не только реактивные потоки , в интеграционном тесте используются реальные компоненты, максимально приближенные к реальным условиям производства. Ценой этого является более высокая потребность в ресурсах и значительно увеличенное время выполнения тестовых наборов.

7. Контрактное тестирование

В слабосвязанной микросервисной архитектуре контракты — это единственное, что публикует и использует каждый сервис. Контракт может быть выражен в IDL, таких как Protocol Buffers или Apache Thrift, что делает его сравнительно простым в общении, развитии и потреблении. Но для веб-API RESTful, основанных на HTTP, это, скорее всего, будет какой-то формой проекта или спецификации. В этом случае возникает вопрос: как потребитель может отстаивать свои ожидания от таких контрактов? И что еще более важно, как поставщик мог развивать контракт, не нарушая существующих потребителей?

Это трудные проблемы, когда потребительское тестирование по контракту может быть очень полезным. Идея довольно проста. Поставщик публикует договор. Потребитель создает тесты, чтобы убедиться, что он правильно интерпретирует договор. Интересно, что потребителю может не понадобиться использовать все API, а только подмножество, которое ему действительно нужно для выполнения работы. И, наконец, потребитель передает эти тесты обратно поставщику. Этот последний шаг очень важен, поскольку он помогает провайдеру развивать API, не мешая потребителям.

В экосистеме JVM Pact JVM и Spring Cloud Contract являются двумя наиболее популярными библиотеками для тестирования контрактов, ориентированного на потребителя . Давайте посмотрим, как портал администрирования клиентов JCG Car Rentals может использовать Pact JVM, чтобы добавить тестовый контракт, управляемый потребителем, для одного из API обслуживания клиентов с использованием публикуемой им спецификации OpenAPI .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class RegistrationApiContractTest {
    private static final String PROVIDER_ID = "Customer Service";
    private static final String CONSUMER_ID = "JCG Car Rentals Admin";
 
    @Rule
    public ValidatedPactProviderRule provider = new ValidatedPactProviderRule(getContract(), null, PROVIDER_ID,
        "localhost", randomPort(), this);
 
    private String getContract() {
        return getClass().getResource("/contract/openapi.json").toExternalForm();
    }
         
    @Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)
    public RequestResponsePact registerCustomer(PactDslWithProvider builder) {
        return builder
            .uponReceiving("registration request")
            .method("POST")
            .path("/customers")
            .body(
                new PactDslJsonBody()
                    .stringType("email")
                    .stringType("firstName")
                    .stringType("lastName")
                    .object("homeAddress")
                        .stringType("streetLine1")
                        .stringType("city")
                        .stringType("postalCode")
                        .stringType("stateOrProvince")
                        .stringType("country")
                        .closeObject()
            )
            .willRespondWith()
            .status(201)
            .matchHeader(HttpHeaders.CONTENT_TYPE, "application/json")
            .body(               
                new PactDslJsonBody()
                    .uuid("id")
                    .stringType("email")
                    .stringType("firstName")
                    .stringType("lastName")
                    .object("homeAddress")
                        .stringType("streetLine1")
                        .stringType("city")
                        .stringType("postalCode")
                        .stringType("stateOrProvince")
                        .stringType("country")
                        .closeObject())
            .toPact();
    }
     
    @Test
    @PactVerification(value = PROVIDER_ID, fragment = "registerCustomer")
    public void testRegisterCustomer() {
        given()
            .contentType(ContentType.JSON)
            .body(Json
                .createObjectBuilder()
                .add("email", "john@smith.com")
                .add("firstName", "John")
                .add("lastName", "Smith")
                .add("homeAddress", Json
                    .createObjectBuilder()
                    .add("streetLine1", "7393 Plymouth Lane")
                    .add("city", "Springfield")
                    .add("postalCode", "19064")
                    .add("stateOrProvince", "PA")
                    .add("country", "United States of America"))
                .build())
            .post(provider.getConfig().url() + "/customers");
    }
}

Существует множество способов написания контрактных тестов, ориентированных на потребителя , и выше — только одна из них. Неважно, какой подход вы будете использовать, качество вашей микросервисной архитектуры улучшится .

Продвигаясь дальше, инструменты, такие как swagger-diff , Swagger Brake и assertj-swagger , очень полезны для проверки изменений в контрактах (поскольку в большинстве случаев это живое существо) и для обеспечения того, чтобы служба правильно выполняла контракт, на который она претендует к.

Если этого недостаточно, одним из бесценных инструментов является Diffy из Twitter, который помогает находить потенциальные ошибки в сервисах, используя запущенные экземпляры новой версии и старой версии рядом. Он ведет себя как прокси-сервер, который направляет все полученные запросы в каждый из запущенных экземпляров, а затем сравнивает ответы.

8. Проверка компонентов

На вершине пирамиды тестирования одного микросервиса находятся тесты компонентов. По сути, они осуществляют реальное, в идеале производственное развертывание, с использованием только внешних служб (или имитированных).

Давайте вернемся в Службу бронирования и пройдемся по тестированию компонентов, которое мы можем предложить. Поскольку он опирается на службу инвентаризации , нам нужно смоделировать эту внешнюю зависимость. Для этого мы могли бы воспользоваться расширением Spring Cloud Contract WireMock, которое, как следует из названия, основано на WireMock . Помимо Inventory Service, мы также высмеиваем провайдера безопасности, используя аннотацию @MockBean .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@AutoConfigureWireMock(port = 0)
@Testcontainers
@SpringBootTest(
    webEnvironment = WebEnvironment.RANDOM_PORT,
    properties = {
        "services.inventory.url=http://localhost:${wiremock.server.port}"
    }
)
class ReservationServiceIT {
    private final String username = "ac2a4b5d-a35f-408e-a652-47aa8bf66bc5";
     
    private final UUID vehicleId = UUID.fromString("4091ffa2-02fa-4f09-8107-47d0187f9e33");
    private final UUID customerId = UUID.fromString(username);
 
    @Autowired private ObjectMapper objectMapper;
    @Autowired private ApplicationContext context;
    @MockBean private ReactiveJwtDecoder reactiveJwtDecoder;
    private WebTestClient webClient;
     
    @Container
    private static final GenericContainer<?> container = new GenericContainer<>("cassandra:3.11.3")
        .withTmpFs(Collections.singletonMap("/var/lib/cassandra", "rw"))
        .withExposedPorts(9042)
        .withStartupTimeout(Duration.ofMinutes(2));
 
    @BeforeEach
    public void setup() {
        webClient = WebTestClient
            .bindToApplicationContext(context)
            .apply(springSecurity())
            .configureClient()
            .build();
    }
     
    @Test
    @DisplayName("Should create Customer reservations")
    public void shouldCreateCustomerReservation() throws JsonProcessingException {
        final OffsetDateTime reserveFrom = OffsetDateTime.now().plusDays(1);
        final OffsetDateTime reserveTo = reserveFrom.plusDays(2);
         
        stubFor(get(urlEqualTo("/" + vehicleId + "/availability"))
            .willReturn(aResponse()
                .withHeader("Content-Type", "application/json")
                .withBody(objectMapper.writeValueAsString(new Availability(vehicleId, true)))));
 
        webClient
            .mutateWith(mockUser(username).roles("CUSTOMER"))
            .mutateWith(csrf())
            .post()
            .uri("/api/reservations")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .body(BodyInserters
                .fromObject(new CreateReservation()
                    .withVehicleId(vehicleId)
                    .withFrom(reserveFrom)
                    .withTo(reserveTo)))
            .exchange()
            .expectStatus().isCreated()
            .expectBody(Reservation.class)
            .value(r -> {
                assertThat(r)
                    .extracting(Reservation::getCustomerId, Reservation::getVehicleId)
                    .containsOnly(vehicleId, customerId);
            });
    }
}

Несмотря на то, что многое происходит под капотом, тестовый пример все еще выглядит вполне управляемым, но время, необходимое для его выполнения, приближается к 50 секундам.

При разработке тестов компонентов, пожалуйста, имейте в виду, что не должно быть никаких ярлыков (как, например, изменение данных в базе данных напрямую). Если вам нужны какие-либо предварительные условия или способ подтверждения состояния внутренней службы, рассмотрите возможность введения поддерживающих API, которые доступны только во время тестирования (включены, например, с использованием профилей или свойств конфигурации).

9. Сквозное тестирование

Целью сквозных тестов является проверка того, что вся система работает как положено, и поэтому предполагается, что все компоненты развернуты в полном объеме. Будучи очень важными, сквозные тесты являются самыми сложными, дорогими, медленными и, как показывает практика, наиболее хрупкими.

Как правило, сквозные тесты разрабатываются после рабочих процессов, выполняемых пользователями, от начала до конца. Из-за этого часто точкой входа в систему является какой-то мобильный или веб-интерфейс, поэтому тестовые среды, такие как Geb , Selenium и Robot Framework, являются здесь довольно популярным выбором.

10. Инъекция неисправностей и проектирование хаоса

Было бы справедливо сказать, что большинство тестов смещены в сторону «счастливого пути» и не исследуют ошибочные сценарии, кроме как тривиальных, таких как, например, запись отсутствует в хранилище данных или ввод недействителен. Как часто вы видели тестовые наборы, которые преднамеренно представляют проблемы с подключением к базе данных?

Как мы уже говорили в предыдущей части урока , плохие вещи случаются и лучше быть готовыми. Дисциплина хаос-инжиниринга породила множество различных библиотек, фреймворков и наборов инструментов для выполнения инжекции ошибок и симуляции.

Чтобы сфабриковать различные виды проблем с сетью, вы можете начать с Blockade , Saboteur или Comcast , все из которых сосредоточены на сбоях в сети и внедрении разделов и направлены на упрощение тестирования устойчивости и стабильности.

Chaos Toolkit — более продвинутый и дисциплинированный подход для проведения экспериментов хаоса. Он также довольно хорошо интегрируется с большинством популярных механизмов оркестровки и облачных провайдеров. В том же духе SimianArmy от Netflix является одним из первых (если не первым) облачно-ориентированных инструментов для генерации различных видов сбоев и обнаружения аномальных условий. Для сервисов, построенных поверх стека Spring Boot , существует специальный проект под названием Chaos Monkey for Spring Boot, о котором вы, возможно, уже слышали. Это довольно молодой, но развивается быстро и очень перспективно.

Этот вид тестирования является довольно новым для большинства организаций, но в контексте микросервисной архитектуры его абсолютно стоит рассмотреть и инвестировать. Эти тесты дают вам уверенность в том, что система способна пережить перебои в работе, постепенно снижая функциональность вместо того, чтобы загореться и загореться. Многие организации (например, Netflix ) регулярно проводят эксперименты с хаосом на производстве, активно выявляя проблемы и исправляя их.

11. Выводы

В этой части урока мы сосредоточились на тестировании. Наше освещение далеко не исчерпывает и не исчерпывает себя, так как есть много разных видов тестов. Во многих отношениях тестирование отдельных микросервисов не сильно отличается, применяются те же передовые методы. Но распределенная природа такой архитектуры приносит много уникальных проблем, которые пытаются решить Контрактное тестирование вместе с Fault Injection и Chaos Engineering .

В заключение, серия статей « Тестирование микросервисов», «Разумный способ» и « Тестирование в производстве», безопасный способ — потрясающие источники отличных идей и советов о том, что работает и как избежать распространенных ошибок при тестировании микросервисов .

12. Что дальше

В следующем разделе учебника мы продолжим тему тестирования и поговорим о тестировании производительности (нагрузка и стресс).