Рубрики
Без рубрики

Практическое применение тестовой пирамиды в микросервисе на базе Spring

Краткий и практический обзор оптимальной пирамиды тестирования.

Автор оригинала: Kumar Chandrakant.

1. Обзор

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

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

2. Давайте Сделаем Шаг Назад

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

Необходимость тестирования программного обеспечения присуща и, возможно, так же стара, как и сама история разработки программного обеспечения. Тестирование программного обеспечения прошло долгий путь от ручного до автоматизированного и далее. Цель, однако, остается прежней — предоставить программное обеспечение, соответствующее спецификациям .

2.1. Виды испытаний

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

Давайте рассмотрим некоторые из популярных и, возможно, однозначных:

  • Модульные тесты : Модульные тесты-это тесты, которые нацелены на небольшие блоки кода, предпочтительно изолированные . Цель здесь состоит в том, чтобы проверить поведение наименьшего проверяемого фрагмента кода, не беспокоясь об остальной части кодовой базы. Это автоматически подразумевает, что любая зависимость должна быть заменена либо макетом, либо заглушкой, либо подобной конструкцией.
  • Интеграционные тесты : В то время как модульные тесты фокусируются на внутренних компонентах кода, факт остается фактом: большая сложность лежит за его пределами. Единицы кода должны работать вместе и часто с внешними службами, такими как базы данных, брокеры сообщений или веб-службы. Интеграционные тесты-это тесты, которые нацелены на поведение приложения при интеграции с внешними зависимостями .
  • Тесты пользовательского интерфейса : Программное обеспечение, которое мы разрабатываем, часто используется через интерфейс, с которым потребители могут взаимодействовать. Довольно часто приложение имеет веб-интерфейс. Однако интерфейсы API становятся все более популярными. Тесты пользовательского интерфейса нацелены на поведение этих интерфейсов, которые часто являются очень интерактивными по своей природе . Теперь эти тесты могут проводиться сквозным способом, или пользовательские интерфейсы также могут тестироваться изолированно.

2.2. Руководство по сравнению Автоматизированные тесты

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

Это еще более важно в гибких методологиях разработки и архитектуре облачных микросервисов. Однако необходимость автоматизации тестирования была осознана гораздо раньше.

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

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

3. Что такое Тестовая Пирамида?

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

Однако, как мы должны решить, сколько тестов мы должны написать для каждого типа? На какие преимущества или подводные камни следует обратить внимание? Это некоторые из проблем, решаемых моделью автоматизации тестирования, такой как пирамида тестов.

Майк Кон придумал конструкцию под названием Тестовая пирамида в своей книге ” Успех с помощью Agile “. Это представляет визуальное представление количества тестов, которые мы должны написать на разных уровнях детализации.

Идея состоит в том, что он должен быть самым высоким на самом детальном уровне и должен начать уменьшаться по мере расширения сферы нашего тестирования. Это придает типичную форму пирамиде, отсюда и название:

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

  • Мы должны писать тесты с разным уровнем детализации
  • Мы должны писать меньше тестов, так как мы становимся более грубыми с их областью применения

4. Инструменты Автоматизации Тестирования

На всех основных языках программирования доступно несколько инструментов для написания различных типов тестов. Мы рассмотрим некоторые из популярных вариантов в мире Java.

4.1. Модульные тесты

  • Платформа тестирования: Наиболее популярным выбором здесь , в Java, является JUnit , который имеет выпуск следующего поколения, известный как JUnit 5 . Другие популярные варианты в этой области включают Тестирование , которое предлагает некоторые отличительные функции по сравнению с JUnit 5. Однако для большинства применений оба эти варианта являются подходящими.
  • Насмешка: Как мы видели ранее, мы определенно хотим вычесть большую часть зависимостей, если не все, при выполнении модульного теста. Для этого нам нужен механизм замены зависимостей тестовым двойником, таким как макет или заглушка. Mockito – отличный фреймворк для создания насмешек над реальными объектами в Java.

4.2. Интеграционные тесты

  • Структура тестирования: Область применения интеграционного теста шире, чем модульного теста, но точкой входа часто является тот же код с более высокой абстракцией. По этой причине те же тестовые платформы, которые работают для модульного тестирования, подходят и для интеграционного тестирования.
  • Насмешка: Цель интеграционного теста-проверить поведение приложения с помощью реальных интеграций. Однако мы, возможно, не захотим обращаться к реальной базе данных или брокеру сообщений для тестирования. Многие базы данных и аналогичные сервисы предлагают встраиваемую версию для написания интеграционных тестов.

4.3. Тесты пользовательского интерфейса

  • Структура тестирования: Сложность тестов пользовательского интерфейса варьируется в зависимости от того, как клиент обрабатывает элементы пользовательского интерфейса программного обеспечения. Например, поведение веб-страницы может отличаться в зависимости от устройства, браузера и даже операционной системы. Селен является популярным выбором для эмуляции поведения браузера с помощью веб-приложения. Однако для API-интерфейсов REST лучше всего использовать такие платформы, как REST-assured .
  • Издевательство: Пользовательские интерфейсы становятся более интерактивными и отображаются на стороне клиента с помощью фреймворков JavaScript, таких как Angular и React . Более разумно тестировать такие элементы пользовательского интерфейса изолированно, используя тестовую платформу, такую как Jasmine и Mocha . Очевидно, что мы должны делать это в сочетании с сквозными тестами.

5. Внедрение принципов на практике

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

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

Мы воспользуемся ими, чтобы продемонстрировать практическое применение тестовой пирамиды.

5.1. Архитектура приложения

Мы разработаем элементарное приложение, которое позволит нам хранить и запрашивать просмотренные фильмы:

Как мы видим, у него есть простой контроллер REST, предоставляющий три конечные точки:

@RestController
public class MovieController {
 
    @Autowired
    private MovieService movieService;
 
    @GetMapping("/movies")
    public List retrieveAllMovies() {
        return movieService.retrieveAllMovies();
    }
 
    @GetMapping("/movies/{id}")
    public Movie retrieveMovies(@PathVariable Long id) {
        return movieService.retrieveMovies(id);
    }
 
    @PostMapping("/movies")
    public Long createMovie(@RequestBody Movie movie) {
        return movieService.createMovie(movie);
    }
}

Контроллер просто перенаправляет данные в соответствующие службы, помимо обработки маршалинга и отмены маршалинга данных:

@Service
public class MovieService {
 
    @Autowired
    private MovieRepository movieRepository;

    public List retrieveAllMovies() {
        return movieRepository.findAll();
    }
 
    public Movie retrieveMovies(@PathVariable Long id) {
        Movie movie = movieRepository.findById(id)
          .get();
        Movie response = new Movie();
        response.setTitle(movie.getTitle()
          .toLowerCase());
        return response;
    }
 
    public Long createMovie(@RequestBody Movie movie) {
        return movieRepository.save(movie)
          .getId();
    }
}

Кроме того, у нас есть репозиторий JPA, который сопоставляется с нашим уровнем сохраняемости:

@Repository
public interface MovieRepository extends JpaRepository {
}

Наконец, наша простая доменная сущность для хранения и передачи данных о фильмах:

@Entity
public class Movie {
    @Id
    private Long id;
    private String title;
    private String year;
    private String rating;

    // Standard setters and getters
}

С помощью этого простого приложения мы теперь готовы исследовать тесты с различной степенью детализации и количеством.

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

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

public class MovieServiceUnitTests {
 
    @InjectMocks
    private MovieService movieService;
 
    @Mock
    private MovieRepository movieRepository;
 
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
 
    @Test
    public void givenMovieServiceWhenQueriedWithAnIdThenGetExpectedMovie() {
        Movie movie = new Movie(100L, "Hello World!");
        Mockito.when(movieRepository.findById(100L))
          .thenReturn(Optional.ofNullable(movie));
 
        Movie result = movieService.retrieveMovies(100L);
 
        Assert.assertEquals(movie.getTitle().toLowerCase(), result.getTitle());
    }
}

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

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

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

@RunWith(SpringRunner.class)
@SpringBootTest
public class MovieControllerIntegrationTests {
 
    @Autowired
    private MovieController movieController;
 
    @Test
    public void givenMovieControllerWhenQueriedWithAnIdThenGetExpectedMovie() {
        Movie movie = new Movie(100L, "Hello World!");
        movieController.createMovie(movie);
 
        Movie result = movieController.retrieveMovies(100L);
 
        Assert.assertEquals(movie.getTitle().toLowerCase(), result.getTitle());
    }
}

Обратите внимание на несколько интересных различий здесь. Теперь мы не высмеиваем никаких зависимостей. Тем не менее, нам все еще может потребоваться создать несколько зависимостей в зависимости от ситуации . Более того, мы проводим эти тесты с помощью Spring Runner .

Это, по сути, означает, что у нас будет контекст приложения Spring и живая база данных для выполнения этого теста. Неудивительно, что это будет работать медленнее! Следовательно, мы выбираем гораздо меньше сценариев для тестирования здесь.

5.4. Тестирование пользовательского интерфейса

Наконец, у нашего приложения есть конечные точки REST для использования, которые могут иметь свои собственные нюансы для тестирования. Поскольку это пользовательский интерфейс для нашего приложения, мы сосредоточимся на нем в нашем тестировании пользовательского интерфейса. Давайте теперь используем REST-assured для тестирования приложения:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MovieApplicationE2eTests {
 
    @Autowired
    private MovieController movieController;
 
    @LocalServerPort
    private int port;
 
    @Test
    public void givenMovieApplicationWhenQueriedWithAnIdThenGetExpectedMovie() {
        Movie movie = new Movie(100L, "Hello World!");
        movieController.createMovie(movie);
 
        when().get(String.format("http://localhost:%s/movies/100", port))
          .then()
          .statusCode(is(200))
          .body(containsString("Hello World!".toLowerCase()));
    }
}

Как мы видим, эти тесты выполняются с запущенным приложением и получают к нему доступ через доступные конечные точки . Мы фокусируемся на тестировании типичных сценариев, связанных с HTTP, таких как код ответа. Это будут самые медленные тесты для выполнения по очевидным причинам.

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

6. Тестовая пирамида для микросервисов

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

Хотя решение этой проблемы в монолитном приложении дает нам желаемую пирамидальную структуру, это может не потребоваться для других архитектур .

Как мы знаем, архитектура микросервиса берет приложение и предоставляет нам набор слабо связанных приложений. При этом он экстернализует некоторые сложности, которые были присущи приложению.

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

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

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

7. Интеграция с CISCO

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

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

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

8. Заключение

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

Кроме того, мы поняли, что означает тестовая пирамида. Мы реализовали это с помощью микросервиса, построенного с использованием Spring Boot.

Наконец, мы рассмотрели актуальность тестовой пирамиды, особенно в контексте архитектуры, такой как микросервисы.