Создаем простейший API и тестируем его с помощью Playwright + TS

67dbf622615c918ed5c9d5924dc772e9.png

Краткое содержание.

Что будет выполнено в ходе данной статьи:
1. Будет создан простейший API сервер на NodeJS для запуска локально.
2. Будут написаны автотесты, на Playwright + Typescript, покрывающие простые запросы GET, POST, PUT, PATCH, DELETE.
3. Выполнены негативные тесты с получением ошибок, последующим анализом и устранением.

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





    
    
    API Server is Running
    


    

API Server Cars is running


Add more data if you needed(e.g. API documentation).

GET ALL

fetch('http://localhost:3000/api/cars/')
 .then(response => response.json())
 .then(data => console.log('GET full Car list:', data))
 .catch(error => console.error('Error:', error));

Данные из этого файла будут отображаться на главной странице.

Конечная структура сервера должна выглядеть так

API сервер

API сервер

Запускаем сервер командой node cars.js и проверяем открыв ссылку в браузере http://localhost:3000/

Главная страница

Главная страница

Проверяем API на работоспособность. Открываем консоль в dev tools — F12, копируем со страницы запрос GET ALL и жмем Enter.

консоль dev tools

консоль dev tools

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

Для простейшего понимания функционала представим что сервер это гараж с автомобилями, а методы GET — посмотреть/воспользоваться авто, POST — приобрести новый авто, DELETE — продать авто, PATCH — тюнинговать/модернизировать авто и PUT — получить авто на подмену.

2. Написание авто тестов Playwright + TS

Для фреймворка Playwright необходимо создать новую директорию, например test_cars_api

  1. Откройте командную строку и перейдите в рабочий каталог или откройте каталог редактором кода (например VSC).

  2. Установите фреймворк Playwright выполнив команду npm init playwright@latest и выбрав необходимые опции (язык typescript, каталог для тестов и т.п.)

  3. Установите дополнительные библиотеки при необходимости
    npm install typescript

  4. Создать новый файл тестов в папке 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

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

Результат тест 1

Результат тест 1

Тест 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"

Результат Тест 2.

Результат Тест 2.

Результат — тест успешно пройден данные соответствуют.

Тест 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"

Результат  - Тест 3

Результат — Тест 3

Результат — тест успешно пройден, новые данные добавлены на сервер.
Отправив запрос GET ALL, сервер вернет расширенный список содержащий новым объект.

Консоль dev tools

Консоль dev tools

Тест 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"

Результат Тест 4

Результат Тест 4

Результат — тест выполнен успешно. Если выполнить запрос GET ALL, то мы увидим что в объекте №2 отсутствует свойство brand.

Консоль dev tools

Консоль dev tools

Тест 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"

Результат Тест 5

Результат Тест 5

Результат — тест выполнен успешно. Если выполнить запрос GET ALL, то мы увидим что объект №3 имеет все 3 свойства и было обновлено только одно — color. Теперь мы видим наглядную разницу между PUT и PATCH.

Консоль dev tools

Консоль dev tools

Тест 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"

Результат Тест 6

Результат Тест 6

Результат — тест выполнен успешно. Если выполнить запрос GET ALL, то мы увидим что объект №4 был удален из списка.

Консоль dev tools

Консоль dev tools

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.

Результат Тест 7.

Результат Тест 7.

Тест 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.

Ошибка Тест 8

Ошибка Тест 8

Обновим тест с учетом специального кейса валидации для метода 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');
    })

Результат — тест выполнен успешно.

Результат Тест 8

Результат Тест 8

Тест 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"

Как результат мы видим не соответствия данных при проверке свойств объекта.

Результат Тест 9.

Результат Тест 9.

На этом все. Надеюсь материал был написан максимально прост к пониманию и повторению практических задач.

© Habrahabr.ru