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

Архитектура Spring services

[отказ от ответственности: в этом посте будут подробно рассмотрены многие аспекты одного возможного типа архитектуры для mi… С пометкой “микросервисы”, “архитектура”, “java”, “веб-разработчик”.

[отказ от ответственности: в этом посте будут подробно рассмотрены многие аспекты одного из возможных типов архитектуры для микросервисов, и для демонстрации примеров будут использоваться Java и SpringBoot. В нем не будет показана полная и полная рабочая настройка Spring Boot, и предполагается, что основные принципы могут быть применены к любому технологическому стеку. Основополагающие идеи – это то, что наиболее важно с точки зрения архитектуры]

Вступление

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

  • Слабо связанные;
  • Высокая ремонтопригодность и простота тестирования;
  • Принадлежит небольшой команде;
  • Структурированный вокруг бизнес-концепций;
  • Обеспечить структурированное и независимое развертывание;

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

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

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

Создание микросервисов в современном мире

В наши дни все быстро развивается и постоянно развивается, и код не является исключением из этого правила. Фактически, согласно сообщению в ArsTechnica, разработчики утверждают, что сегодня управляют в 100 раз большим количеством кода , чем в 2010 году. Это огромный прирост. И эта тенденция продолжает расти! Поскольку все больше и больше компаний встают на путь цифровой трансформации, мы теперь видим разработчиков и вакансии разработчиков в компаниях, основной сферой деятельности которых даже не является техническая: розничная торговля, страхование и даже продовольственный бизнес в настоящее время вносят свой вклад в внедрение современного кода.

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

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

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

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

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

Монолитная линза

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

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

Объектив микросервисов

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

  • Платежный сервис;
  • Служба управления пользователями;
  • Сервис корзины покупок;
  • Служба листинга продуктов;
  • (…)

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

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

Платежный сервис , UserManagementService , ShoppingCartService , Служба листинга товаров , …

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

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

Также важно отметить, что микросервисы очень хорошо подходят для работы в среде CI/CD благодаря быстрому циклу обратной связи и более высокой модульности и доступности. Сегодня очень легко отправлять API-интерфейсы в виде служб docker через конвейеры CI/CD и доставлять код намного быстрее, чем при использовании монолита. Это делает почти обязательным в настоящее время разработку архитектур с использованием микросервисов там, где это разрешено и допустимо.

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

Основные компоненты архитектуры кода наших микросервисов

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

При проектировании архитектуры микросервисов с использованием Spring Boot и Java должны быть установлены некоторые базовые строительные блоки. Первое, что нужно обсудить, – это то, как на самом деле предоставить архитектуру микросервиса для использования клиентами:

Это будет сделано с помощью REST API, который предоставит выделенные конечные точки, которые затем делегируют логику нашим службам (отсюда и название microservices) и вернут полезную нагрузку JSON в качестве ответа. Таким образом, любой клиент, будь то веб-приложение, мобильное приложение и т.д., Может просто вызвать предоставляемый нами API и получить ответы в формате JSON, которые затем могут быть отображены на веб-странице, мобильном приложении и т.д.

REST API – это поверхностный уровень, который взаимодействует между клиентами и нашей архитектурой микросервисов, поэтому теперь нам нужно подробно описать, как будет структурирован фактический код микросервиса.

Детализация архитектуры микросервисного кода

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

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

Обработка запросов с помощью Spring’s @RestController аннотация

Когда запрос поступает от клиента, точкой входа будет то, что называется “классом ресурсов”. В Spring Boot класс ресурсов – это класс, который определяет URL-адреса конечных точек, которые будут отображаться, когда клиент запрашивает ресурс.

Схема такого класса может быть такой, как показано ниже:

@RestController
public class ShoppingCartResource {

    private final PaymentService paymentService;
    private final ProductListingService productListingService;

    public ShoppingCartResource(PaymentService paymentService,
        ProductListingService productListingService) {
        this.paymentService = paymentService;
        this.productListingService = productListingService;
    }

    @GetMapping(value = "/list-products", produces =
        MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity> getProducts() {
        return productListingService.getAllProducts()
            .map(product -> ResponseEntity.ok().body(product))
            .orElse(ResponseEntity.notFound().build());

    @PostMapping(value = "/checkout/{cartId}", produces =
        MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity processPayment(@PathVariable String cartId) {
        return paymentService.performPayment(cartId);
 }

На уровне архитектуры кода уже многое происходит, даже в

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

Мы можем видеть, что методы конечной точки аннотируются такими аннотациями, как @GetMapping и @PostMapping . Это означает, что существует сопоставление URL-адреса с определенным Http-глаголом для определенного URL-адреса. Если мы посмотрим на /list-products выше, мы увидим, что он определен как отображение Get. Это означает, что запросы, отправленные по этому URL-адресу, будут иметь формат:

request:
Http GET https:///list-products

и мы также заявляем, что ответ будет в формате JSON. Никакие другие методы не разрешены в этой конкретной конечной точке.

Обоснование для конечной точки @Portmapping было бы аналогичным.

Автоматическое подключение и внедрение сервисов в класс ресурсов

Выше мы видим, что созданный нами класс ресурсов содержит службы, введенные в конструктор.

В последних версиях Spring это фактически делает то же самое, что и наличие аннотации @Autowired в этих классах, что означает, что, поскольку мы передаем эти службы в качестве параметров конструктора нашему классу ресурсов, Spring будет знать, какие службы инициализировать и создавать экземпляры, чтобы все работало.

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

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

Мы уже можем видеть первый уровень “косвенности” в нашей архитектуре:

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

Давайте теперь углубимся в то, что представляет собой сервис в нашей архитектуре.

Структура службы

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

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

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

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

@Service
public class ProductListingService {
    private static final Log LOG = LogFactory.getLog(ProductListingService.class);

    private ProductRepository productRepository;

    public ProductUpdatingService(ProductRepository productRepository) {
        this.productRepository = productRepository;
}

public List listProducts() {
      return newArrayList(productRepository.findAll());
}

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

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

Службы несут ответственность за перевод форматов данных между прикладным уровнем и уровнем сохраняемости

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

Введение шаблона репозитория в качестве абстракции поверх уровня сохраняемости

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

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

Примером класса репозитория является следующий:

@Repository
public interface ProductRepository extends CrudRepository {
  //this already offers methods inherited from CrudRepository: findAll(), findById(), deleteById() and updateById()
}

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

@Repository
public interface ProductRepository extends CrudRepository {
      @Query("select p from Product p where p.name in (:productNames)")
      List retrieveProductsUsingTheirNames(@Param("productNames") List productNames);
}

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

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

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

Различные форматы данных между уровнем сохраняемости и прикладным уровнем

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

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

Чтобы разделить эти проблемы, служба фактически “переводит” между различными представлениями данных, используя то, что известно как “Объекты передачи данных”, или сокращенно DTO.

Основная идея заключается в том, что, если мы извлекаем Product из базы данных, неплохо было бы иметь ProductDTO где-нибудь на уровне сервиса, который мы будем использовать в дальнейшем.

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

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

@Entity
public class Product implements Serializable {
    private long id;
    private String name;
    private long stock;
    private Supplier supplier;

    public Product() {
    }

    public Product(String name, long stock, Supplier supplier) {
        this.name = name;
        this.stock = stock;
        this.supplier = supplier;
    }

    @Id
    @GeneratedValue(generator = "product_id_seq", strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(name = "product_id_seq", sequenceName = "product_id_seq", schema = "product", allocationSize = 1)
    @Column(name = "id", nullable = false, updatable = false)
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "supplier_id", referencedColumnName = "id", nullable = false)
    public Supplier getSupplier() {
        return supplier;
    }

    public void setSupplier(Supplier supplier) {
        this.supplier = supplier;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    } 

    public long getStock() {
        return stock;
    }

    public void setStock(long stock) {
        this.stock = stock;
    } 

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

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

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

Итак, чтобы сохранить базу данных изолированной от остального кода, а также облегчить нашу жизнь, мы можем создать так называемый класс DTO.

Мы можем рассматривать класс DTO как линзу, через которую наше приложение “видит” базу данных. Код приложения просто связан с точным форматом, требуемым клиентами нашего API, и один из подходов к отражению этого требования в нашем коде заключается в использовании представления, которое точно соответствует этому. Это DTO. Вот как будет выглядеть DTO для продукта в нашем примере домена:

public class ProductDTO {

    private String name;
    private long stock;
    private String supplierName;

    public ProductDTO() {}

    public ProductDTO(String name, long stock, String supplierName) {
        this.name = name;
        this.stock = stock;
        this.supplierName = supplierName;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSupplierName() {
        return supplierName;
    }

    public void setSupplierName(String supplierName) {
        this.supplierName = supplierName;
    }

    public long getStock() {
        return stock;
    }

    public void setStock(long stock) {
        this.stock = stock;
    }
}

Это уже выглядит проще! И что еще более важно, он также намного ближе к нашему домену, и с ним действительно легко работать, теперь, когда весь багаж базы данных отсутствует. Это имеет решающее значение для гибкого и поддерживаемого кода. Полностью контролируя представление DTO, мы теперь можем гораздо проще управлять вещами на прикладном уровне. Мы можем создавать и расширять DTO, мы можем выполнять любые сложные запросы на уровне базы данных, какие захотим и мы знаем, что наши ДАННЫЕ смогут адаптироваться к любым потребностям. Это возвращает контроль в наши руки.

Если вы все еще помните, в начале наш сервис для перечисления продуктов возвращал List . Это связано с тем, что службы заботятся о том, что клиенты хотят получить от API, а репозитории могут беспокоиться о базе данных. Взаимодействие между двумя мирами происходит на уровне обслуживания.

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

Рекомендации по тестированию архитектуры, ориентированной на микросервисы

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

Услуги модульного тестирования

Давайте начнем с рассмотрения того, как мы можем проводить модульное тестирование сервисов в нашей установке.

Мы видим, что служба возвращает DTO после манипулирования определенным объектом БД через репозиторий, поэтому идеальный способ модульного тестирования службы самостоятельно – это имитировать определенный ответ из репозитория и утверждать результирующий возврат службы, чтобы убедиться, что его внутренняя логика правильно реализовано. Итак, план таков:

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

  • Используя эти контролируемые данные, мы также можем утверждать, что наш сервис выдает правильные DTO, утверждая его содержимое;

  • Подтвердите правильность взаимодействия с классом репозитория, убедившись, что метод репозитория вызывается только один раз;

Давайте посмотрим, как это выглядит на практике:

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@ExtendWith(MockitoExtension.class)
class ProductListingServiceTest {

    private ProductListingService productListingService;

    @Mock
    private ProductRepository productRepository;

    @BeforeEach
    void setUp() {
        this.productListingService = new ProductListingService(productRepository);

        Product product = new Product();
        product.setName("test");
        product.setStock(100L);
        product.setSupplier(new Supplier("someSupplier"));
        Product product2 = new Product();
        product.setName("test2");
        product.setStock(101L);
        product.setSupplier(new Supplier("someSupplier2"));
}

   @Test
    void listProduct_lists_correctly() {
        doReturn(newArrayList(product,product2).toIterable()).when(productRepository).findAll();
        List list = productListingService.listProducts();

        verify(productRepository, times(1)).findAll();
        assertThat(list.size(),2);
    }

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

Аннотация @ExtendWith(MockitoExtension.class ) используется, чтобы убедиться, что mocks можно настроить в тестовом контексте, просто аннотируя их с помощью @Mock , как показано выше.

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

Давайте рассмотрим интеграционное тестирование с помощью Spring MockMvc .

Использование MockMvc для написания интеграционных тестов

Теперь мы заинтересованы в тестировании нашего API с точки зрения конечной точки, то есть с уровня ресурсов.

Для того, чтобы сделать это, мы можем использовать MockMvc .

MockMvc – это класс Spring, который мы можем использовать для написания интеграционных тестов для наших конечных точек. По сути, после некоторой минимальной проводки мы можем имитировать запрос к нашим конечным точкам API, точно так же, как он поступил бы от внешнего клиента, и утверждать его возвращаемый статус, значение и другие вещи, чтобы протестировать API более “e2e”. мода.

Для настройки MockMvc нам просто нужны некоторые аннотации в нашем тестовом классе следующим образом:

@AutoConfigureMockMvc
@SpringBootTest
class ProductsResourceTest {

    @Autowired
    private MockMvc mockMvc;
    (...)

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

Как только это настроено, мы видим, что экземпляр MockMvc позволяет нам отправлять http-запросы и утверждать ответы, сохраняя при этом определенные внешние зависимости под нашим контролем.

Примером такого запроса может быть:

@AutoConfigureMockMvc
@SpringBootTest
class ShoppingCartResourceTest {

    @Autowired
    private MockMvc mockMvc;

     @Mock
    private ProductListingService productListingService;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(
            new ProductResource(productListingService))
            .build();
    }

    @Test
    void listProducts_with_empty_repository_returns_OK_with_empty_list() throws Exception {

        MockMvc result = mockMvc.perform(get("/list-products")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .andExpect(status().isOk());

        assertTrue(result.andReturn().getResponse().getContentAsString().equals("{}"));
    }

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

Вывод

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

Оригинал: “https://dev.to/brunooliveira/architecturing-microservices-8n2”