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

Разработка на основе тестов для API-интерфейсов Spring Boot

Автор оригинала: Robley Gori.

Вступление

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

Как это возможно? Интерфейсы прикладного программирования (API) отвечают за это расширенное подключение. Они позволяют мобильным и веб-приложениям взаимодействовать и облегчают передачу данных между ними и другими системами.

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

Появление API

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

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

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

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

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

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

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

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

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

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

Создание более совершенных API

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

Например, Twitter предоставил некоторые из своих API для использования другими разработчиками для создания других клиентов Twitter и использования платформы другими уникальными способами. Некоторые создали ботов на таких платформах, как Telegram, для отправки твитов или получения твитов, все это достигается с помощью API.

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

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

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

Они разделены на пять категорий, которые включают коды для:

  • Информационные ответы : |/1xx статусы, такие как 100 Продолжение , 101 Протокол переключения и т.д. Успех
  • : 2xx статусы, такие как 200 ОК , 202 Принято и т.д. Перенаправление
  • : 3xx статусы, такие как 300 Вариантов выбора , 301 Перемещен навсегда и т.д. Ошибки клиента
  • : 4xx статусы, такие как 400 Неправильный запрос , 403 Запрещен , 404 Не найден и т.д. Ошибки сервера
  • : 5xx статусы, такие как 500 Внутренних ошибок сервера , 502 Плохой шлюз , 503 Недоступна служба и т.д.

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

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

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

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

Создание API

Архитектура проекта

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

Используя фреймворк Spring Boot, мы можем создать единый API, который может обслуживать все три приложения или клиента одновременно.

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

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

Инструмент позволяет нам выбирать между Maven и Gradle , которые являются инструментами, помогающими нам автоматизировать некоторые аспекты нашего рабочего процесса сборки, такие как тестирование, запуск и упаковка нашего Java-приложения. Мы также получаем возможность выбирать между использованием Java или Kotlin при создании нашего API с помощью Spring Boot, для которого мы можем указать версию.

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

Для нашего API мы выберем следующие зависимости:

  • Web чтобы помочь нам разработать веб-API
  • MySQL который поможет нам подключиться к вашей базе данных MySQL,
  • JPA , который является API сохранения Java для удовлетворения наших потребностей во взаимодействии с базой данных, и
  • Привод , чтобы помочь нам поддерживать и контролировать наше веб-приложение.

Установив зависимости, мы нажимаем кнопку “Создать проект”, чтобы получить почтовый индекс, содержащий наш стандартный код.

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

$ tree .
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pbcopy
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── cars
    │   │               └── CarsApplication.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── com
                └── example
                    └── cars
                        └── CarsApplicationTests.java

В корневой папке есть pom.xml файл, содержащий конфигурацию проекта для нашего API загрузки Spring. Если бы мы использовали Gradle, вместо этого у нас был бы файл build.gradle . Он включает в себя такую информацию, как детали нашего нового API и все его зависимости.

В основном мы будем работать в папках main и test внутри папки source ( src ). Именно здесь мы разместим наши контроллеры, модели, служебные классы и другие.

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

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

$ mysql -u root -p

mysql> CREATE DATABASE cars_database;
Query OK, 1 row affected (0.08 sec)

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

Файлы конфигурации позволяют нам легко переключать такие детали, что облегчает миграцию и изменение нашего API. Это достигается с помощью файла конфигурации, который в API загрузки Spring является файлом application.properties , расположенным в папке src/main/resources .

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

# Database Properties
spring.datasource.url = jdbc:mysql://localhost:3306/cars_database?useSSL=false
spring.datasource.username = root
spring.datasource.password = password

# Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

Теперь нам нужен класс сущностей для определения ресурсов нашего API и их сведений, которые будут сохранены в нашей базе данных. A Car является вашим ресурсом в этом API, и это означает, что он представляет наш объект или объект реальной жизни, с информацией о котором мы будем выполнять действия. Такие действия включают Создание, чтение, обновление и удаление, проще говоря, операции CRUD|/.

Эти операции лежат в основе методов HTTP или Глаголов , которые относятся к различным операциям, которые может предоставлять API. Они включают в себя:

Git Essentials

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

  • GET – это операция чтения, которая извлекает только указанные данные,
  • СООБЩЕНИЕ , которое позволяет создавать ресурсы путем предоставления их информации в рамках запроса,
  • ПОМЕСТИТЕ , который позволяет нам изменять ресурс, и
  • УДАЛИТЬ , который мы используем для удаления ресурса и его информации из нашего API.

Чтобы лучше организовать наш код, мы представим еще несколько папок в нашем проекте на уровне src/main/java/com/пример/автомобили/ . Мы добавим папку с именем модели для размещения классов, определяющих наши объекты.

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

$ tree .
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pbcopy
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── cars
    │   │               ├── CarsApplication.java
    │   │               ├── controllers
    │   │               ├── models
    │   │               ├── repository
    │   │               └── utils
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── com
                └── example
                    └── cars
                        └── CarsApplicationTests.java

Модель предметной области

Давайте определим наш Автомобиль класс в папке модели :

/**
* This class will represent our car and its attributes
*/
@Entity
// the table in the database tht will contain our cars data
@EntityListeners(AuditingEntityListener.class)
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private long id; // Each car will be given an auto-generated unique identifier when stored

    private String carName; // We will also save the name of the car

    private int doors; // We will also save the number of doors that a car has

    // getters and setters
}

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

ДАО

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

public interface CarRepository extends JpaRepository { }

Написание Тестов

Теперь мы можем предоставить функциональность нашего API через наш контроллер , но в духе Разработки на основе тестов (TDD) давайте сначала напишем тесты в файле CarsApplicationTests :

// These are a subset of the tests, the full test file is available on the Github repo attached at the end of this article
....

    /**
     * Here we test that we can get all the cars in the database
     * using the GET method
     */
    @Test
    public void testGetAllCars() {
        HttpHeaders headers = new HttpHeaders();
        HttpEntity entity = new HttpEntity(null, headers);

        ResponseEntity response = restTemplate.exchange(getRootUrl() + "/cars",
            HttpMethod.GET, entity, String.class);

        Assert.assertNotNull(response.getBody());
    }

    /**
     * Here we test that we can fetch a single car using its id
     */
    @Test
    public void testGetCarById() {
        Car car = restTemplate.getForObject(getRootUrl() + "/cars/1", Car.class);
        System.out.println(car.getCarName());
        Assert.assertNotNull(car);
    }

    /**
     * Here we test that we can create a car using the POST method
     */
    @Test
    public void testCreateCar() {
        Car car = new Car();
        car.setCarName("Prius");
        car.setDoors(4);

        ResponseEntity postResponse = restTemplate.postForEntity(getRootUrl() + "/cars", car, Car.class);
        Assert.assertNotNull(postResponse);
        Assert.assertNotNull(postResponse.getBody());
    }

    /**
     * Here we test that we can update a car's information using the PUT method
     */
    @Test
    public void testUpdateCar() {
        int id = 1;
        Car car = restTemplate.getForObject(getRootUrl() + "/cars/" + id, Car.class);
        car.setCarName("Tesla");
        car.setDoors(2);

        restTemplate.put(getRootUrl() + "/cars/" + id, car);

        Car updatedCar = restTemplate.getForObject(getRootUrl() + "/cars/" + id, Car.class);
        Assert.assertNotNull(updatedCar);
    }

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

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

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

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

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

Контроллер

Давайте теперь реализуем нашу функциональность API в Автомобильном контроллере , который находится в папке контроллеры :

@RestController
@RequestMapping("/api/v1")
public class CarController {

    @Autowired
    private CarRepository carRepository;

    // GET Method for reading operation
    @GetMapping("/cars")
    public List getAllCars() {
        return carRepository.findAll();
    }

    // GET Method for Read operation
    @GetMapping("/cars/{id}")
    public ResponseEntity getCarsById(@PathVariable(value = "id") Long carId)
        throws ResourceNotFoundException {

        Car car = carRepository
                  .findById(carId)
                  .orElseThrow(() -> new ResourceNotFoundException("Car not found on :: " + carId));
        return ResponseEntity.ok().body(car);
    }

    // POST Method for Create operation
    @PostMapping("/cars")
    public Car createCar(@Valid @RequestBody Car car) {
        return carRepository.save(car);
    }

    // PUT Method for Update operation
    @PutMapping("/cars/{id}")
    public ResponseEntity updateCar(
        @PathVariable(value = "id") Long carId, @Valid @RequestBody Car carDetails)
        throws ResourceNotFoundException {
            Car car = carRepository
                      .findById(carId)
                      .orElseThrow(() -> new ResourceNotFoundException("Car " + carId + " not found"));

        car.setCarName(carDetails.getCarName());
        car.setDoors(carDetails.getDoors());

        final Car updatedCar = carRepository.save(car);
        return ResponseEntity.ok(updatedCar);
    }

    // DELETE Method for Delete operation
    @DeleteMapping("/car/{id}")
    public Map deleteCar(@PathVariable(value = "id") Long carId) throws Exception {
        Car car = carRepository
                  .findById(carId)
                  .orElseThrow(() -> new ResourceNotFoundException("Car " + carId + " not found"));

        carRepository.delete(car);
        Map response = new HashMap<>();
        response.put("deleted", Boolean.TRUE);
        return response;
    }
}

Вверху у нас есть @RestController аннотация для определения нашего автомобильного контроллера класса в качестве контроллера для нашего API Spring Boot. Далее следует @RequestMapping , где мы указываем базовый путь нашего URL-адреса API как /api/v1 . Это также включает в себя версию.

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

Ранее мы узнали об операциях Создания, чтения, обновления и удаления в API и о том, как они сопоставляются с методами HTTP. Эти методы включены в структуру Spring как Сопоставление записей , Сопоставление GetMapping , Сопоставление Put и Удаление сопоставлений аннотаций соответственно. Каждая из этих аннотаций помогает нам предоставлять конечные точки, которые выполняют только указанную операцию CRUD.

У нас также может быть одна конечная точка, которая обрабатывает различные методы HTTP:

@RequestMapping(value="/cars", method = { RequestMethod.GET, RequestMethod.POST })

Теперь, когда мы реализовали эту функциональность, давайте проведем наши тесты:

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

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

Мы начинаем с извлечения всех автомобилей, которые мы сохранили в нашей базе данных:

На старте у нас не было припасенных автомобилей. Давайте добавим нашу первую машину:

Ответ-это идентификатор и детали автомобиля, которые мы только что добавили. Если мы добавим еще несколько автомобилей и соберем все машины, которые мы сохранили:

Это автомобили, которые мы создали с помощью нашего API Spring Boot. Быстрая проверка базы данных возвращает тот же список:

Пользовательский интерфейс Swagger

Мы создали и протестировали наш API с использованием TDD, и теперь , чтобы улучшить наш API, мы собираемся документировать его с помощью Swagger UI , что позволяет нам создавать автоматически создаваемый интерфейс для взаимодействия с другими пользователями и изучения нашего API.

Во-первых, позвольте нам добавить следующие зависимости в ваш pom.xml :


  io.springfox
  springfox-swagger2
  2.7.0



  io.springfox
  springfox-swagger-ui
  2.7.0

Далее мы создадим SwaggerConfig.java в той же папке, что и CarsApplication.java , которая является точкой входа в наш API.

В SwaggerConfig.java файл позволяет также добавить некоторую информацию о нашем API:

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.example.cars"))
            .paths(PathSelectors.any())
            .build()
            .apiInfo(metadata());
    }

    /**
     * Adds metadata to Swagger
     *
     * @return
     */
    private ApiInfo metadata() {
        return new ApiInfoBuilder()
            .title("Cars API")
            .description("An API to store car details built using Spring Boot")
            .build();
    }
}

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

// Add this import in our controller file...
import io.swagger.annotations.ApiOperation;

// ...then annotate our HTTP Methods
@PostMapping("/...") // Our endpoint

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

В нашем случае мы также будем использовать класс Car для форматирования и проверки параметров нашего запроса. Поэтому мы аннотируем его “добытчики” следующим образом:

    public long getId() {
        return id;
    }

    public String getCarName() {
        return carName;
    }

    public int getDoors() {
        return doors;
    }

Вот и все! Наша документация готова. Когда мы запускаем наш API с помощью mvn spring-boot:запустите и перейдите к http://localhost:8080/swagger-ui.html мы можем ознакомиться с документацией нашего API:

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

Вывод

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

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

Исходный код этого проекта доступен здесь, на Github .