Refactor petstore tests and refresh build setup
This commit is contained in:
39
.gitignore
vendored
39
.gitignore
vendored
@@ -1,41 +1,6 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
*.iml
|
||||
.DS_Store
|
||||
/.idea/
|
||||
/.mvn/
|
||||
/.gitignore
|
||||
|
||||
130
README.md
130
README.md
@@ -1,80 +1,64 @@
|
||||
# Автотесты для Petstore API
|
||||
# OTUS Rest-assured Homework 3
|
||||
|
||||
Этот проект содержит набор автотестов для [Swagger Petstore API](https://petstore.swagger.io/v2) с использованием библиотек **Rest-assured** и **JUnit 5**. Проект оформлен как Maven-проект с поддержкой JDK 24.
|
||||
## Цель проекта
|
||||
Автоматизировать API-сценарии для `https://petstore.swagger.io` с использованием Rest-assured, JUnit Jupiter и quality gates (Checkstyle + SpotBugs).
|
||||
|
||||
## Стек технологий
|
||||
- Java 21
|
||||
- Maven
|
||||
- Rest-assured `6.0.0`
|
||||
- JUnit Jupiter `6.0.2`
|
||||
- Jackson Databind `2.21.0`
|
||||
- Commons Codec `1.21.0`
|
||||
- Maven Surefire Plugin `3.5.4`
|
||||
- Maven Compiler Plugin `3.15.0`
|
||||
- Checkstyle (`maven-checkstyle-plugin` `3.6.0`)
|
||||
- SpotBugs (`spotbugs-maven-plugin` `4.9.8.2`)
|
||||
|
||||
## Реализованные сценарии
|
||||
1. `POST /pet` + `GET /pet/{id}`:
|
||||
- создание питомца;
|
||||
- повторное чтение по id;
|
||||
- проверка, что вернулся именно созданный объект.
|
||||
|
||||
2. `GET /pet/{id}`:
|
||||
- проверка схемы ответа через Json Schema Validator.
|
||||
|
||||
3. Негативные сценарии:
|
||||
- `POST /pet` с malformed JSON возвращает ошибку;
|
||||
- `GET /pet/{id}` для удаленного/несуществующего id возвращает `404`;
|
||||
- `GET /pet/findByStatus` с невалидным статусом возвращает пустой список.
|
||||
|
||||
## Архитектура
|
||||
- 2-уровневый дизайн:
|
||||
- `service` слой (HTTP-вызовы + спецификации);
|
||||
- `tests` слой (сценарии).
|
||||
- Тесты создают сервис напрямую (`new PetService()`), без DI-фреймворка.
|
||||
|
||||
## Структура проекта
|
||||
```plaintext
|
||||
hw3/
|
||||
├── pom.xml # Maven-конфигурация
|
||||
├── README.md # Readme файл
|
||||
└── src
|
||||
└── test
|
||||
└── java
|
||||
└── ru
|
||||
└── otus
|
||||
└── petstore
|
||||
└── PetStoreTests.java # Автотесты
|
||||
- `src/main/java/ru/otus/petstore/config` — конфигурация
|
||||
- `src/main/java/ru/otus/petstore/model` — модели API (`Pet`, `Category`, `Tag`)
|
||||
- `src/main/java/ru/otus/petstore/service` — сервисы API
|
||||
- `src/test/java/ru/otus/petstore/tests` — автотесты (позитивные и негативные сценарии)
|
||||
- `src/test/java/ru/otus/petstore/util` — фабрика тестовых данных
|
||||
- `src/test/resources/schemas` — JSON schema
|
||||
|
||||
## Запуск
|
||||
### 1. Только тесты
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
## Обзор
|
||||
### 2. Полная проверка (тесты + Checkstyle + SpotBugs)
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
**Rest-assured** — это библиотека для тестирования REST API, предоставляющая удобный DSL (domain-specific language)
|
||||
для формирования HTTP-запросов, их отправки и проверки ответов. В данном проекте с её помощью отправляются GET и POST
|
||||
запросы к Petstore API, а также производятся проверки кодов ответа и содержимого JSON.
|
||||
## Параметры запуска
|
||||
- `base.uri` (по умолчанию `https://petstore.swagger.io`)
|
||||
- `base.path` (по умолчанию `/v2`)
|
||||
|
||||
### Реализованные сценарии тестирования
|
||||
|
||||
1. **Тест: Поиск питомцев со статусом "available"**
|
||||
Отправляется GET-запрос на `/pet/findByStatus` с параметром `status=available`.
|
||||
**Ожидаемый результат:** HTTP 200 и у всех питомцев в ответе значение поля `status` равно `"available"`.
|
||||
|
||||
2. **Тест: Поиск питомцев по несуществующему статусу**
|
||||
Отправляется GET-запрос с параметром `status=invalidStatus`.
|
||||
**Ожидаемый результат:** HTTP 200 и пустой массив в ответе.
|
||||
|
||||
3. **Тест: Успешное создание питомца**
|
||||
Отправляется POST-запрос с корректными данными для создания питомца.
|
||||
**Ожидаемый результат:** HTTP 200 и возвращаемое поле `name` совпадает с переданным значением.
|
||||
|
||||
4. **Тест: Ошибка при создании питомца без поля "name"**
|
||||
Отправляется POST-запрос, в котором отсутствует обязательное поле `name`.
|
||||
**Ожидаемый результат:** API возвращает ошибку (код 400 или 405).
|
||||
**PS.** Почему-то сервис отдает ответ 200 я решил оставить данный тест.
|
||||
Показать что вся цепочка тестов не останавливается и продолжает работать дальше
|
||||
|
||||
5. **Тест: Ошибка при передаче некорректного JSON**
|
||||
Отправляется POST-запрос с ошибочным JSON (без закрывающей фигурной скобки).
|
||||
**Ожидаемый результат:** API возвращает ошибку (код 400 или 405).
|
||||
|
||||
## Как запустить тесты
|
||||
|
||||
1. **Клонируйте репозиторий:**
|
||||
```bash
|
||||
git clone https://git.kovbasa.ru/otus-autotests/hw3.git
|
||||
|
||||
2. **Перейдите в папку проекта:**
|
||||
|
||||
```bash
|
||||
cd hw3
|
||||
|
||||
3. **Запустите тесты командой:**
|
||||
```bash
|
||||
mvn test
|
||||
|
||||
После выполнения тестов в консоли вы увидите нумерованные сообщения с результатами (например, "1. Test: Find pets by
|
||||
'available' status - Passed").
|
||||
|
||||
## Зависимости
|
||||
- **Rest-assured**: 4.5.1
|
||||
|
||||
- **JUnit Jupiter (API & Engine)**: 5.9.1
|
||||
|
||||
## Лицензия
|
||||
|
||||
Проект предназначен для образовательных целей.
|
||||
```
|
||||
---
|
||||
Файл REDME.md содержит краткое описание проекта, объясняет суть реализации тестов с Rest-assured, описывает
|
||||
основные сценарии тестирования, структуру проекта и шаги для запуска автотестов. Вы можете его адаптировать под
|
||||
свои нужды или дополнить дополнительной информацией.
|
||||
```
|
||||
Пример:
|
||||
```bash
|
||||
mvn "-Dbase.uri=https://petstore.swagger.io" "-Dbase.path=/v2" test
|
||||
```
|
||||
|
||||
34
checkstyle.xml
Normal file
34
checkstyle.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<module name="Checker">
|
||||
<property name="charset" value="UTF-8"/>
|
||||
<property name="severity" value="error"/>
|
||||
|
||||
<module name="FileTabCharacter"/>
|
||||
|
||||
<module name="LineLength">
|
||||
<property name="max" value="120"/>
|
||||
</module>
|
||||
|
||||
<module name="TreeWalker">
|
||||
<module name="TypeName"/>
|
||||
<module name="MethodName"/>
|
||||
<module name="ParameterName"/>
|
||||
<module name="LocalVariableName"/>
|
||||
<module name="MemberName"/>
|
||||
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="UnusedImports"/>
|
||||
<module name="RedundantImport"/>
|
||||
|
||||
<module name="NeedBraces"/>
|
||||
|
||||
<module name="WhitespaceAround"/>
|
||||
<module name="WhitespaceAfter"/>
|
||||
|
||||
<module name="EmptyBlock"/>
|
||||
</module>
|
||||
</module>
|
||||
120
pom.xml
120
pom.xml
@@ -1,37 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- Основная информация о проекте -->
|
||||
<groupId>ru.otus</groupId>
|
||||
<artifactId>petstore</artifactId>
|
||||
<artifactId>hw3</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>petstore</name>
|
||||
<name>hw3</name>
|
||||
|
||||
<!-- Глобальные свойства проекта -->
|
||||
<properties>
|
||||
<!-- Используем Java 24 -->
|
||||
<maven.compiler.source>24</maven.compiler.source>
|
||||
<maven.compiler.target>24</maven.compiler.target>
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- Версии зависимостей -->
|
||||
<restassured.version>4.5.1</restassured.version>
|
||||
<junit.jupiter.version>5.9.1</junit.jupiter.version>
|
||||
<base.uri>https://petstore.swagger.io</base.uri>
|
||||
<base.path>/v2</base.path>
|
||||
|
||||
<restassured.version>6.0.0</restassured.version>
|
||||
<junit.jupiter.version>6.0.2</junit.jupiter.version>
|
||||
<jackson.version>2.21.0</jackson.version>
|
||||
|
||||
<maven.compiler.version>3.15.0</maven.compiler.version>
|
||||
<surefire.version>3.5.4</surefire.version>
|
||||
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
|
||||
<spotbugs.plugin.version>4.9.8.2</spotbugs.plugin.version>
|
||||
<spotbugs.version>4.9.8</spotbugs.version>
|
||||
</properties>
|
||||
|
||||
<!-- Зависимости проекта -->
|
||||
<dependencies>
|
||||
<!-- Rest-assured для работы с REST API с исключением транзитивой зависимости commons-codec -->
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<version>${restassured.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-codec</groupId>
|
||||
@@ -40,56 +41,111 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Явное подключение обновлённой, безопасной версии commons-codec -->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.15</version>
|
||||
<scope>test</scope>
|
||||
<version>1.21.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON-парсинг через Rest-assured -->
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
<version>${restassured.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>json-schema-validator</artifactId>
|
||||
<version>${restassured.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit Jupiter API для написания автотестов (включает ExtensionContext и TestWatcher) -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit Jupiter Engine для выполнения тестов -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<!-- Конфигурация сборки проекта -->
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Maven Compiler Plugin для компиляции проекта с поддержкой Java 24 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<version>${maven.compiler.version}</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<release>${java.version}</release>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Maven Surefire Plugin для запуска тестов -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
<version>${surefire.version}</version>
|
||||
<configuration>
|
||||
<useModulePath>false</useModulePath>
|
||||
<systemPropertyVariables>
|
||||
<base.uri>${base.uri}</base.uri>
|
||||
<base.path>${base.path}</base.path>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${checkstyle.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle-validation</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<configLocation>checkstyle.xml</configLocation>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
<version>${spotbugs.plugin.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs</artifactId>
|
||||
<version>${spotbugs.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
6
spotbugs-exclude.xml
Normal file
6
spotbugs-exclude.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FindBugsFilter>
|
||||
<Match>
|
||||
<Source name="~.*src/test/java/.*"/>
|
||||
</Match>
|
||||
</FindBugsFilter>
|
||||
21
src/main/java/ru/otus/petstore/config/TestConfig.java
Normal file
21
src/main/java/ru/otus/petstore/config/TestConfig.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package ru.otus.petstore.config;
|
||||
|
||||
public final class TestConfig {
|
||||
|
||||
private static final String BASE_URI =
|
||||
System.getProperty("base.uri", "https://petstore.swagger.io");
|
||||
|
||||
private static final String BASE_PATH =
|
||||
System.getProperty("base.path", "/v2");
|
||||
|
||||
private TestConfig() {
|
||||
}
|
||||
|
||||
public static String getBaseUri() {
|
||||
return BASE_URI;
|
||||
}
|
||||
|
||||
public static String getBasePath() {
|
||||
return BASE_PATH;
|
||||
}
|
||||
}
|
||||
23
src/main/java/ru/otus/petstore/model/Category.java
Normal file
23
src/main/java/ru/otus/petstore/model/Category.java
Normal file
@@ -0,0 +1,23 @@
|
||||
// Category.java
|
||||
package ru.otus.petstore.model;
|
||||
|
||||
public class Category {
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public Category() { }
|
||||
|
||||
public Category(long id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Category(Category other) {
|
||||
this.id = other.id;
|
||||
this.name = other.name;
|
||||
}
|
||||
public long getId() { return id; }
|
||||
public void setId(long id) { this.id = id; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
}
|
||||
58
src/main/java/ru/otus/petstore/model/Pet.java
Normal file
58
src/main/java/ru/otus/petstore/model/Pet.java
Normal file
@@ -0,0 +1,58 @@
|
||||
// Pet.java
|
||||
package ru.otus.petstore.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Pet {
|
||||
private long id;
|
||||
private Category category;
|
||||
private String name;
|
||||
private List<String> photoUrls;
|
||||
private List<Tag> tags;
|
||||
private String status;
|
||||
|
||||
public Pet() { }
|
||||
|
||||
public Pet(long id, Category category, String name,
|
||||
List<String> photoUrls, List<Tag> tags, String status) {
|
||||
this.id = id;
|
||||
this.category = copyCategory(category);
|
||||
this.name = name;
|
||||
this.photoUrls = copyPhotoUrls(photoUrls);
|
||||
this.tags = copyTags(tags);
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public void setId(long id) { this.id = id; }
|
||||
public Category getCategory() { return copyCategory(category); }
|
||||
public void setCategory(Category category) { this.category = copyCategory(category); }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public List<String> getPhotoUrls() { return copyPhotoUrls(photoUrls); }
|
||||
public void setPhotoUrls(List<String> photoUrls) { this.photoUrls = copyPhotoUrls(photoUrls); }
|
||||
public List<Tag> getTags() { return copyTags(tags); }
|
||||
public void setTags(List<Tag> tags) { this.tags = copyTags(tags); }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
private static Category copyCategory(Category source) {
|
||||
return source == null ? null : new Category(source);
|
||||
}
|
||||
|
||||
private static List<String> copyPhotoUrls(List<String> source) {
|
||||
return source == null ? null : List.copyOf(source);
|
||||
}
|
||||
|
||||
private static List<Tag> copyTags(List<Tag> source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
List<Tag> target = new ArrayList<>(source.size());
|
||||
for (Tag tag : source) {
|
||||
target.add(new Tag(tag));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
23
src/main/java/ru/otus/petstore/model/Tag.java
Normal file
23
src/main/java/ru/otus/petstore/model/Tag.java
Normal file
@@ -0,0 +1,23 @@
|
||||
// Tag.java
|
||||
package ru.otus.petstore.model;
|
||||
|
||||
public class Tag {
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public Tag() { }
|
||||
|
||||
public Tag(long id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Tag(Tag other) {
|
||||
this.id = other.id;
|
||||
this.name = other.name;
|
||||
}
|
||||
public long getId() { return id; }
|
||||
public void setId(long id) { this.id = id; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
}
|
||||
36
src/main/java/ru/otus/petstore/service/BaseService.java
Normal file
36
src/main/java/ru/otus/petstore/service/BaseService.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package ru.otus.petstore.service;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.filter.log.LogDetail;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import ru.otus.petstore.config.TestConfig;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
|
||||
public abstract class BaseService {
|
||||
|
||||
protected final RequestSpecification requestSpec = new RequestSpecBuilder()
|
||||
.setBaseUri(TestConfig.getBaseUri())
|
||||
.setBasePath(TestConfig.getBasePath())
|
||||
.setContentType(ContentType.JSON)
|
||||
.log(LogDetail.URI)
|
||||
.log(LogDetail.METHOD)
|
||||
.log(LogDetail.PARAMS)
|
||||
.log(LogDetail.BODY)
|
||||
.build();
|
||||
|
||||
protected final ResponseSpecification okSpec = new ResponseSpecBuilder()
|
||||
.expectStatusCode(200)
|
||||
.build();
|
||||
|
||||
protected final ResponseSpecification notFoundSpec = new ResponseSpecBuilder()
|
||||
.expectStatusCode(404)
|
||||
.build();
|
||||
|
||||
protected RequestSpecification givenBase() {
|
||||
return given().spec(requestSpec);
|
||||
}
|
||||
}
|
||||
72
src/main/java/ru/otus/petstore/service/PetService.java
Normal file
72
src/main/java/ru/otus/petstore/service/PetService.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package ru.otus.petstore.service;
|
||||
|
||||
import io.restassured.response.Response;
|
||||
import ru.otus.petstore.model.Pet;
|
||||
|
||||
public class PetService extends BaseService {
|
||||
|
||||
public Response findByStatus(String status) {
|
||||
return givenBase()
|
||||
.queryParam("status", status)
|
||||
.when()
|
||||
.get("/pet/findByStatus");
|
||||
}
|
||||
|
||||
public Response createPet(Pet pet) {
|
||||
return givenBase()
|
||||
.body(pet)
|
||||
.when()
|
||||
.post("/pet");
|
||||
}
|
||||
|
||||
public Response createPetExpectSuccess(Pet pet) {
|
||||
return createPet(pet)
|
||||
.then()
|
||||
.spec(okSpec)
|
||||
.extract()
|
||||
.response();
|
||||
}
|
||||
|
||||
public Response createPetRaw(String rawJson) {
|
||||
return givenBase()
|
||||
.body(rawJson)
|
||||
.when()
|
||||
.post("/pet");
|
||||
}
|
||||
|
||||
public Response getPetById(long id) {
|
||||
return givenBase()
|
||||
.when()
|
||||
.get("/pet/{id}", id);
|
||||
}
|
||||
|
||||
public Response getPetByIdExpectSuccess(long id) {
|
||||
return getPetById(id)
|
||||
.then()
|
||||
.spec(okSpec)
|
||||
.extract()
|
||||
.response();
|
||||
}
|
||||
|
||||
public Response getPetByIdExpectNotFound(long id) {
|
||||
return getPetById(id)
|
||||
.then()
|
||||
.spec(notFoundSpec)
|
||||
.extract()
|
||||
.response();
|
||||
}
|
||||
|
||||
public Response deletePet(long id) {
|
||||
return givenBase()
|
||||
.when()
|
||||
.delete("/pet/{id}", id);
|
||||
}
|
||||
|
||||
public void deletePetIfExists(long id) {
|
||||
Response response = deletePet(id);
|
||||
int statusCode = response.statusCode();
|
||||
if (statusCode != 200 && statusCode != 404) {
|
||||
throw new IllegalStateException("Unexpected status for DELETE /pet/{id}: " + statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package ru.otus.petstore;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.TestWatcher;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
@ExtendWith(PetStoreTests.TestResultLogger.class)
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class PetStoreTests {
|
||||
|
||||
/**
|
||||
* Выполняется один раз перед запуском всех тестов.
|
||||
* Устанавливаем базовый URI для запросов к Swagger Petstore API.
|
||||
*/
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
RestAssured.baseURI = "https://petstore.swagger.io/v2";
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест 1: GET /pet/findByStatus (status = available).
|
||||
* Ожидается статус 200 и все объекты в ответе должны иметь поле "status" равное "available".
|
||||
*/
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("1. Test: Find pets by 'available' status")
|
||||
public void testFindPetsByAvailableStatus() {
|
||||
given()
|
||||
.queryParam("status", "available")
|
||||
.when()
|
||||
.get("/pet/findByStatus")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("status", everyItem(equalTo("available")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест 2: GET /pet/findByStatus с несуществующим статусом "invalidStatus".
|
||||
* Ожидается статус 200 и пустой массив в ответе.
|
||||
*/
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("2. Test: Find pets by invalid status")
|
||||
public void testFindPetsByInvalidStatus() {
|
||||
given()
|
||||
.queryParam("status", "invalidStatus")
|
||||
.when()
|
||||
.get("/pet/findByStatus")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("", hasSize(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест 3: POST /pet для создания питомца с валидными данными.
|
||||
* Ожидается статус 200 и возвращаемое поле "name" совпадает с переданным.
|
||||
*/
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("3. Test: Create pet successfully")
|
||||
public void testCreatePetSuccess() {
|
||||
int petId = (int) (System.currentTimeMillis() % Integer.MAX_VALUE);
|
||||
String petName = "Rex";
|
||||
String requestBody = "{\n" +
|
||||
" \"id\": " + petId + ",\n" +
|
||||
" \"category\": { \"id\": 1, \"name\": \"Dogs\" },\n" +
|
||||
" \"name\": \"" + petName + "\",\n" +
|
||||
" \"photoUrls\": [ \"http://example.com/photo.jpg\" ],\n" +
|
||||
" \"tags\": [ { \"id\": 0, \"name\": \"string\" } ],\n" +
|
||||
" \"status\": \"available\"\n" +
|
||||
"}";
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(requestBody)
|
||||
.when()
|
||||
.post("/pet")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("name", equalTo(petName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест 4: POST /pet для создания питомца с невалидными данными (без поля "name").
|
||||
* Ожидается, что API вернёт ошибку (400 или 405). Почему-то сервис отдает ответ 200
|
||||
* я решил оставить данный тест. Показать что вся цепочка тестов не останавливается и
|
||||
* продолжает работать дальше
|
||||
*/
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("4. Test: Fail to create pet without 'name' field")
|
||||
public void testCreatePetMissingName() {
|
||||
int petId = (int) (System.currentTimeMillis() % Integer.MAX_VALUE);
|
||||
String requestBody = "{\n" +
|
||||
" \"id\": " + petId + ",\n" +
|
||||
" \"category\": { \"id\": 1, \"name\": \"Dogs\" },\n" +
|
||||
" \"photoUrls\": [ \"http://example.com/photo.jpg\" ],\n" +
|
||||
" \"tags\": [ { \"id\": 0, \"name\": \"string\" } ],\n" +
|
||||
" \"status\": \"available\"\n" +
|
||||
"}";
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(requestBody)
|
||||
.when()
|
||||
.post("/pet")
|
||||
.then()
|
||||
.statusCode(anyOf(is(400), is(405)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест 5: POST /pet с некорректным JSON (отсутствует закрывающая фигурная скобка).
|
||||
* Ожидается, что API вернёт ошибку (400 или 405).
|
||||
*/
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("5. Test: Create pet with invalid JSON")
|
||||
public void testCreatePetInvalidJson() {
|
||||
String invalidJson = """
|
||||
{
|
||||
"id": 123456,
|
||||
"name": "Invalid Pet",
|
||||
"category": { "id": 1, "name": "Dogs" },
|
||||
"photoUrls": [ "http://example.com/photo.jpg" ],
|
||||
"tags": [ { "id": 0, "name": "string" } ],
|
||||
"status": "available"
|
||||
"""; // отсутствует закрывающая фигурная скобка
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(invalidJson)
|
||||
.when()
|
||||
.post("/pet")
|
||||
.then()
|
||||
.statusCode(anyOf(is(400), is(405)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Реализация TestWatcher для логирования результата каждого теста.
|
||||
* Выводит сообщения: если тест пройден – "Test Passed", иначе "Test Failed", "Test Aborted" или "Test Disabled".
|
||||
*/
|
||||
public static class TestResultLogger implements TestWatcher {
|
||||
|
||||
@Override
|
||||
public void testSuccessful(ExtensionContext context) {
|
||||
System.out.println(context.getDisplayName() + " - Passed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testFailed(ExtensionContext context, Throwable cause) {
|
||||
System.out.println(context.getDisplayName() + " - Failed: " + cause.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testAborted(ExtensionContext context, Throwable cause) {
|
||||
System.out.println(context.getDisplayName() + " - Aborted");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testDisabled(ExtensionContext context, Optional<String> reason) {
|
||||
System.out.println(context.getDisplayName() + " - Disabled: " + reason.orElse("No reason provided"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package ru.otus.petstore.tests;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.petstore.model.Pet;
|
||||
import ru.otus.petstore.service.PetService;
|
||||
import ru.otus.petstore.util.PetFactory;
|
||||
|
||||
public class PetNegativeScenariosTest {
|
||||
|
||||
private final PetService petService = new PetService();
|
||||
|
||||
/**
|
||||
* Проверяем POST /pet с некорректным JSON:
|
||||
* - API не принимает сломанное тело запроса;
|
||||
* - возвращается клиентская/серверная ошибка вместо 200.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("POST /pet with malformed JSON returns error status")
|
||||
void createPetWithMalformedJsonReturnsError() {
|
||||
String brokenJson = """
|
||||
{
|
||||
"id": 123456,
|
||||
"name": "Broken pet",
|
||||
"photoUrls": ["https://petstore.test/photo.jpg"]
|
||||
""";
|
||||
|
||||
int status = petService.createPetRaw(brokenJson).statusCode();
|
||||
MatcherAssert.assertThat(status, Matchers.anyOf(Matchers.is(400), Matchers.is(405), Matchers.is(500)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяем GET /pet/{id} для несуществующего ресурса:
|
||||
* - сервер возвращает 404;
|
||||
* - сообщение об ошибке соответствует "Pet not found".
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("GET /pet/{id} returns 404 for unknown id")
|
||||
void getMissingPetReturnsNotFound() {
|
||||
long unknownId = PetFactory.nextId();
|
||||
Pet createdPet = PetFactory.newAvailablePet(unknownId);
|
||||
|
||||
// Удаляем id на старте теста, чтобы гарантировать отсутствие сущности.
|
||||
petService.deletePetIfExists(unknownId);
|
||||
|
||||
// Создаем и сразу удаляем питомца, чтобы не пересекаться с возможными данными окружения.
|
||||
petService.createPetExpectSuccess(createdPet);
|
||||
petService.deletePetIfExists(unknownId);
|
||||
|
||||
var response = petService.getPetByIdExpectNotFound(unknownId);
|
||||
MatcherAssert.assertThat(response.path("message"), Matchers.equalTo("Pet not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяем GET /pet/findByStatus с невалидным фильтром:
|
||||
* - API возвращает успешный код;
|
||||
* - выдача пустая.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("GET /pet/findByStatus with unknown status returns empty list")
|
||||
void findByInvalidStatusReturnsEmpty() {
|
||||
petService.findByStatus("invalidStatus")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("$", Matchers.empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package ru.otus.petstore.tests;
|
||||
|
||||
import io.restassured.response.Response;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.otus.petstore.model.Pet;
|
||||
import ru.otus.petstore.service.PetService;
|
||||
import ru.otus.petstore.util.PetFactory;
|
||||
|
||||
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class PetPositiveScenariosTest {
|
||||
|
||||
private final PetService petService = new PetService();
|
||||
|
||||
/**
|
||||
* Проверяем сценарий POST /pet + GET /pet/{id}:
|
||||
* - питомец создается успешно;
|
||||
* - по id возвращается именно созданный объект.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("POST /pet creates resource and GET /pet/{id} returns created pet")
|
||||
void createPetAndGetById() {
|
||||
long petId = PetFactory.nextId();
|
||||
Pet pet = PetFactory.newAvailablePet(petId);
|
||||
|
||||
try {
|
||||
Response createResponse = petService.createPetExpectSuccess(pet);
|
||||
assertThat(createResponse.path("id"), equalTo((int) petId));
|
||||
assertThat(createResponse.path("name"), equalTo(pet.getName()));
|
||||
|
||||
Response getResponse = retryGetPetById(petService, petId);
|
||||
assertThat(getResponse.path("id"), equalTo((int) petId));
|
||||
assertThat(getResponse.path("name"), equalTo(pet.getName()));
|
||||
} finally {
|
||||
petService.deletePetIfExists(petId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяем GET /pet/{id}:
|
||||
* - успешный ответ для существующего питомца;
|
||||
* - тело ответа соответствует JSON-схеме Pet.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("GET /pet/{id} returns valid payload by schema")
|
||||
void getPetByIdMatchesSchema() {
|
||||
long petId = PetFactory.nextId();
|
||||
Pet pet = PetFactory.newAvailablePet(petId);
|
||||
|
||||
try {
|
||||
petService.createPetExpectSuccess(pet);
|
||||
|
||||
petService.getPetByIdExpectSuccess(petId)
|
||||
.then()
|
||||
.body(matchesJsonSchemaInClasspath("schemas/pet-schema.json"));
|
||||
} finally {
|
||||
petService.deletePetIfExists(petId);
|
||||
}
|
||||
}
|
||||
|
||||
private Response retryGetPetById(PetService petService, long petId) {
|
||||
int maxAttempts = 5;
|
||||
for (int i = 0; i < maxAttempts; i++) {
|
||||
Response response = petService.getPetById(petId);
|
||||
if (response.statusCode() == 200) {
|
||||
return response;
|
||||
}
|
||||
sleep(300L);
|
||||
}
|
||||
return petService.getPetByIdExpectSuccess(petId);
|
||||
}
|
||||
|
||||
private void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Sleep interrupted", interruptedException);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/test/java/ru/otus/petstore/util/PetFactory.java
Normal file
30
src/test/java/ru/otus/petstore/util/PetFactory.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package ru.otus.petstore.util;
|
||||
|
||||
import ru.otus.petstore.model.Category;
|
||||
import ru.otus.petstore.model.Pet;
|
||||
import ru.otus.petstore.model.Tag;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public final class PetFactory {
|
||||
private static final String TEST_PHOTO_URL = "https://petstore.test/photo.jpg";
|
||||
|
||||
private PetFactory() {
|
||||
}
|
||||
|
||||
public static long nextId() {
|
||||
return ThreadLocalRandom.current().nextLong(1_000_000L, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static Pet newAvailablePet(long id) {
|
||||
return new Pet(
|
||||
id,
|
||||
new Category(1L, "Dogs"),
|
||||
"Rex-" + id,
|
||||
List.of(TEST_PHOTO_URL),
|
||||
List.of(new Tag(1L, "api-test")),
|
||||
"available"
|
||||
);
|
||||
}
|
||||
}
|
||||
52
src/test/resources/schemas/pet-schema.json
Normal file
52
src/test/resources/schemas/pet-schema.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"photoUrls"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"category": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"photoUrls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user