Краткое содержание
Что будет выполнено в ходе данной статьи:
Будет создан простейший API сервер на NodeJS для запуска локально.
Будут написаны автотесты, на Playwright + Typescript, покрывающие простые запросы GET, POST, PUT, PATCH, DELETE.
Выполнены негативные тесты с получением ошибок, последующим анализом и устранением.
1. Подготовка среды для тестирования - создание API сервера
Основой для выполнения тестов будет примитивный API сервер на NodeJS содержащий объекты в JSON с несколькими свойствами.
Для примера создадим папку для сервера и назовем ее cars-api
Далее следует сделать этот каталог рабочим при помощи консоли cd cars-api
или например открыть каталог программой VSC.
Убедитесь что NodeJS установлен проверив версию node -v
, при необходимости установите NodeJS.
Инициализируйте проект и установите фреймворк express
- npm init -y
- npm install express
Следующим шагом создайте файл cars.js в директории cars-api
const express = require('express');
const path = require('path'); // Import path module
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON requests
app.use(express.json());
// Serve static files from the "public" directory
app.use(express.static('public'));
// Root route to serve index.html
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Sample cars data
let cars = [
{ id: 1, brand: 'Subaru', model: 'Impreza WRX', color: 'Blue'},
{ id: 2, brand: 'Nissan', model: 'Skyline', color: 'Black'},
{ id: 3, brand: 'Toyota', model: 'Supra', color: 'Yellow'}
];
// GET all cars
app.get('/api/cars', (req, res) => {
res.json(cars);
});
// GET car by ID
app.get('/api/cars/:id', (req, res) => {
const car = cars.find(i => i.id === parseInt(req.params.id));
if (car) {
res.json(car);
} else {
res.status(404).json({ message: 'car not found' });
}
});
// POST a new car
app.post('/api/cars', (req, res) => {
const newcar = {
id: cars.length + 1,
brand: req.body.brand,
model: req.body.model,
color: req.body.color,
};
cars.push(newcar);
res.status(201).json(newcar);
});
// PUT (update) an car by ID
app.put('/api/cars/:id', (req, res) => {
const car = cars.find(i => i.id === parseInt(req.params.id));
if ('engine' in req.body) {
return res.status(501).json({ message: 'Not Implemented' });
}
if (car) {
car.brand = req.body.brand;
car.model = req.body.model;
car.color = req.body.color;
res.json(car);
} else {
res.status(404).json({ message: 'car not found' });
}
});
// DELETE a car by ID
app.delete('/api/cars/:id', (req, res) => {
cars = cars.filter(i => i.id !== parseInt(req.params.id));
res.status(204).end();
});
// PATCH (partial update) a car by ID
app.patch('/api/cars/:id', (req, res) => {
const car = cars.find(i => i.id === parseInt(req.params.id));
if (car) {
// Only update fields that are provided in the request body
if (req.body.brand) {
car.brand = req.body.brand;
}
if (req.body.model) {
car.model = req.body.model;
}
if (req.body.color) {
car.color = req.body.color;
}
res.json(car);
} else {
res.status(404).json({ message: 'Item not found' });
}
});
// Status endpoint
app.get('/api/status', (req, res) => {
res.json({ status: 'Server is running' });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}/`);
});
В данном файле мы описываем правила нашего API.
Объектом будет автомобиль с несколькими свойствами (Производитель, модель и цвет). По список будет иметь всего 3 объекта.
let cars = [
{ id: 1, brand: 'Subaru', model: 'Impreza WRX', color: 'Blue'},
{ id: 2, brand: 'Nissan', model: 'Skyline', color: 'Black'},
{ id: 3, brand: 'Toyota', model: 'Supra', color: 'Yellow'}
];
Методы используемые на сервере
- GET (ALL) получить весь список объектов
- GET (ID) получить данные конкретного объекта по ID
- POST создать новый объектов
- PUT заменить конкретный объект по ID
- DELETE удалить конкретный объект по ID
- PATCH заменить часть свойства конкретного объекта по ID
По умолчанию сервер будет запущен на порте 3000 http://localhost:3000/
Далее необходимо создать index.html файл в под каталоге public
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Server is Running</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
header {
background-color: #4CAF50;
color: white;
padding: 15px;
width: 100%;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.name {
color: black;
}
.doc {
text-align: left;
margin-left: 30px;
}
</style>
</head>
<body>
<header>
<h1>API Server <a class="name">Cars</a> is running</h1>
</header>
<br>
<h3>Add more data if you needed(e.g. API documentation).</h3>
<div class ="doc">
<h3>GET ALL </h3>
<p>
fetch('http://localhost:3000/api/cars/')<br>
 .then(response => response.json())<br>
 .then(data => console.log('GET full Car list:', data))<br>
 .catch(error => console.error('Error:', error));<br>
</p>
</div>
</body>
</html>
Данные из этого файла будут отображаться на главной странице.
Конечная структура сервера должна выглядеть так
Запускаем сервер командой node cars.js
и проверяем открыв ссылку в браузере http://localhost:3000/
Проверяем API на работоспособность. Открываем консоль в dev tools - F12, копируем со страницы запрос GET ALL и жмем Enter.
Результат - мы получили список автомобилей, которые хранятся на нашем сервере.
Для простейшего понимания функционала представим что сервер это гараж с автомобилями а методы GET - посмотреть/воспользоваться авто, POST - приобрести новый авто, DELETE - продать авто, PATCH - тюнинговать/модернизировать авто и PUT - получить авто на подмену.
2. Написание авто тестов Playwright + TS
Для фреймворка Playwright необходимо создать новую директорию, например test_cars_api
Откройте командную строку и перейдите в рабочий каталог или откройте каталог редактором кода (например VSC).
Установите фреймворк Playwright выполнив команду
npm init playwright@latest
и выбрав необходимые опции (язык typescript, каталог для тестов и т.п.)Установите дополнительные библиотеки при необходимости
npm install typescript
Создать новый файл тестов в папке test / test_cars_api.spec.ts
import {test, expect, request} from '@playwright/test'
test.describe('API test Cars', () => {
let baseURL = 'http://localhost:3000/api/cars';
})
Первоначально добавим импорт необходимых элементов из playwright и базовую URL.
В файле настроек playwright.config.ts можно поправить конфиг для выполнения тестов только в одном браузере например name: 'chromium', остальные закомментировать.
Тест 1 - Get All проверка получения полного списка
Отправляем GET запрос на весь список, проверяем что статус и количество объектов в списке. Для перепроверки выводим список в консоль.
test('Get All Cars', async ({ request }) =>{
const response = await request.get(baseURL);
expect(response.status()).toBe(200);
const cars = await response.json();
expect(cars.length).toBe(3);
console.log(cars);
})
Переменной response присваивается значение запрос get по базовой URL, которая уже была создана. Далее проверяется статус, значение равно 200 (ok). Затем переменной cars назначается ответ в json формате. В конце проверяется что полученный список содержит 3 автомобиля length = 3.
Запускаем тест npx playwright test
Результат мы видим список автомобилей с их свойствами, тест пройден успешно.
Тест 2 - Get by ID. Получения данных о конкретном объекте
Отправляем GET запрос на первый объект с id = 1, проверяем что статус равен 200 и что результат в свойствах объекта соответствует ожидаемым.
test('Get Car by ID', async ({request}) => {
const carID = 1;
const response = await request.get(`${baseURL}/${carID}`);
expect(response.status()).toBe(200);
const car = await response.json();
expect(car).toEqual({
id:1,
brand: 'Subaru',
model: 'Impreza WRX',
color: 'Blue'
});
console.log(car);
})
В данном тесте в GET запросе мы используем модифицированный базовый URL для получения данных конкретного автомобиля из списка - переменная carID. Последняя операция проверяет полученные свойства объекта с ожидаемыми.
Запускаем второй тест по названию npx playwright test -g "Get Car by ID"
Результат - тест успешно пройден данные соответствуют.
Тест 3 - POST. Создание нового объекта
Отправляем POST запрос и создаем новый объект, 4й в нашем списке.
test('POST new Car', async ({request}) =>{
const newCar = {
brand: 'Honda',
model: 'NSX',
color: 'Yellow'
};
const response = await request.post(baseURL, {data: newCar});
expect(response.status()).toBe(201);
const createdCar = await response.json();
expect(createdCar).toMatchObject(newCar);
console.log(createdCar);
})
В данном тесте объявляем новую переменную newCar и при помощи POST запроса передаем данные на сервер по базовой URL. Проверяем что сервер вернул нам статус 201 (created) и сравниваем данные объекта чтобы убедиться в корректности данных.
Запускаем тест по названию npx playwright test -g "POST new Car"
Результат - тест успешно пройден, новые данные добавлены на сервер.
Отправив запрос GET ALL, сервер вернет расширенный список содержащий новым объект.
Тест 4 - PUT. Замена объекта
Отправляем PUT запрос и передаем объект с 2 свойствами из 3х.
test('PUT existing car', async({request}) => {
const carID = 2;
const updatedCar = {
model: '240 SX',
color: 'Green'
};
const response = await request.put(`${baseURL}/${carID}`, {data: updatedCar});
expect(response.status()).toBe(200);
const car = await response.json();
expect(car).toMatchObject(updatedCar);
console.log(car);
})
В данном тесте используем переменную carID чтобы сервер понимал какой из объектов будет заменен. Объявляем переменную updatedCar, которая имеет 2 свойства из 3х базовых (*для примера) и при помощи PUT запроса передаем данные на сервер. Проверяем что сервер вернул нам статус 200 (ok) и сравниваем данные объекта чтобы убедиться в корректности данных.
Запускаем тест по названию npx playwright test -g "PUT existing car"
Результат - тест выполнен успешно. Если выполнить запрос GET ALL, то мы увидим что в объекте №2 отсутствует свойство brand.
Тест 5 - PATCH. Обновление свойства объекта
Отправляем PATCH запрос и передаем данные для замены 1 из свойств объекта.
test('PATCH property of existing Car', async ({request}) =>{
const carID = 3;
const updatedCar = {
color: 'Red'
};
const response = await request.patch(`${baseURL}/${carID}`, {data: updatedCar});
expect(response.status()).toBe(200);
const car = await response.json();
expect(car).toMatchObject(updatedCar);
console.log(car);
})
В данном тесте используем переменную carID чтобы сервер понимал в каком из объектов будет выполнены изменения. Объявляем переменную updatedCar, которая имеет 1 свойство из 3х базовых и при помощи PATCH запроса передаем данные на сервер. Проверяем что сервер вернул нам статус 200 (ok) и сравниваем данные объекта чтобы убедиться в корректности данных.
Запускаем тест по названию npx playwright test -g "PATCH property of existing Car"
Результат - тест выполнен успешно. Если выполнить запрос GET ALL, то мы увидим что объект №3 имеет все 3 свойства и было обновлено только одно - color. Теперь мы видим наглядную разницу между PUT и PATCH.
Тест 6 - DELETE. Удаление объекта
Отправляем DELETE запрос и удаляем объект.
test('DELETE car by ID', async ({request}) => {
const carID = 4;
const response = await request.delete(`${baseURL}/${carID}`);
expect(response.status()).toBe(204);
const verifyResponse = await request.get(`${baseURL}/${carID}`);
expect(verifyResponse.status()).toBe(404);
})
В данном тесте используем переменную carID чтобы сервер понимал в какой из объектов следует удалить. Отправляем запрос DELETE и проверяем, что статус 204(No content). Следующей операцией отправляем GET запрос на URL удаленного объекта и проверяем что статус равен 404(Not found).
Запускаем тест по названию npx playwright test -g "DELETE car by ID"
Результат - тест выполнен успешно. Если выполнить запрос GET ALL, то мы увидим что объект №4 был удален из списка.
3. Негативные сценарии и анализ ошибок
Тест 7 - GET. Запрос на не существующий объект
test('Negative Get Car by ID', async ({request}) => {
const carID = 4;
const response = await request.get(`${baseURL}/${carID}`);
expect(response.status()).toBe(404);
})
})
По умолчанию сервер содержит 3 объекта. Отправим GET запрос для не существующего 4-го объекта. В результате мы получим статус равный 404. Данный тест содержит те практически те же условия что и Тест 6, но для того чтобы убедиться в том что тест работает корректно заменим значение carID на 1.
Результат - ошибка полученный статус не соответствует ожидаемому. Так как мы заменили carID на существующий объект сервер вернул нам статус 200.
Тест 8 - PUT. Запрос на добавление не существующего свойства
test('Negative PUT non-existing property', async ({request}) => {
const carID = 3;
const updatedCar = {
engine: 'Turbo 3.0'
};
const response = await request.put(`${baseURL}/${carID}`, {data: updatedCar});
expect(response.status()).toBe(200);
})
На пример мы хотим изменить 3й объект добавить свойство engine,так как у нас нет доступа к документации мы ожидаем положительный результат и статус 200.
В результате тест не пройден, получена ошибка статус 501 вместо ожидаемого 200.
Обновим тест с учетом специального кейса валидации для метода PUT, который оказывается есть на сервере. При отправке свойства engine мы ожидаем статус 501c описанием ошибки - Not Implemented.
test('Negative PUT non-existing property', async ({request}) => {
const carID = 3;
const updatedCar = {
engine: 'Turbo 3.0'
};
const response = await request.put(`${baseURL}/${carID}`, {data: updatedCar});
expect(response.status()).toBe(501);
const responseBody = await response.json();
expect(responseBody.message).toBe('Not Implemented');
})
Результат - тест выполнен успешно.
Тест 9 - POST .Специальный кейс некорректная логика на сервере
Для этого теста необходимо внести изменения на сервере - сделать ошибку в обработке POST запроса. Поменяем местами brand и color.
const newcar = {
id: cars.length + 1,
brand: req.body.color,
model: req.body.model,
color: req.body.brand,
};
Перезапускаем сервер. Запускаем уже созданный ранее Тест 3 - POST new Car.npx playwright test -g "POST new Car"
Как результат мы видим не соответствия данных при проверке свойств объекта.
На этом все. Надеюсь материал был написан максимально прост к пониманию и повторению практических задач.