Темы



Создаем простейший 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