Refactor petstore tests and refresh build setup

This commit is contained in:
2026-02-15 14:02:49 +03:00
parent 32f7be7cf0
commit b97c369f46
16 changed files with 655 additions and 316 deletions

39
.gitignore vendored
View File

@@ -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

128
README.md
View File

@@ -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
## Обзор
**Rest-assured** — это библиотека для тестирования REST API, предоставляющая удобный DSL (domain-specific language)
для формирования HTTP-запросов, их отправки и проверки ответов. В данном проекте с её помощью отправляются GET и POST
запросы к Petstore API, а также производятся проверки кодов ответа и содержимого JSON.
### Реализованные сценарии тестирования
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. **Запустите тесты командой:**
## Запуск
### 1. Только тесты
```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, описывает
основные сценарии тестирования, структуру проекта и шаги для запуска автотестов. Вы можете его адаптировать под
свои нужды или дополнить дополнительной информацией.
### 2. Полная проверка (тесты + Checkstyle + SpotBugs)
```bash
mvn verify
```
## Параметры запуска
- `base.uri` (по умолчанию `https://petstore.swagger.io`)
- `base.path` (по умолчанию `/v2`)
Пример:
```bash
mvn "-Dbase.uri=https://petstore.swagger.io" "-Dbase.path=/v2" test
```

34
checkstyle.xml Normal file
View 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>

118
pom.xml
View File

@@ -1,37 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
<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">
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
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<Source name="~.*src/test/java/.*"/>
</Match>
</FindBugsFilter>

View 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;
}
}

View 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; }
}

View 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;
}
}

View 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; }
}

View 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);
}
}

View 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);
}
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View 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"
);
}
}

View 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"
}
}
}