Create a simple API and test it with Playwright + TS

Summary.

What will be accomplished in this article:
1. The simplest one will be created API server on NodeJS to run locally.
2. Autotests will be written, Playwright + Typescriptcovering simple requests GET, POST, PUT, PATCH, DELETE.
3. Negative tests were performed with errors obtained, followed by analysis and elimination.

1. Preparing the testing environment – creating an API server.

The basis for running tests will be a primitive API server on NodeJS containing objects in JSON with several properties.
For example, let's create a folder for the server and name it cars-api
Next, you should make this directory working using the console. cd cars-apior, for example, open the catalog using the VSC program.
Make sure NodeJS is installed by checking the version node -v install NodeJS if necessary.
Initialize the project and install the framework express
npm init -y
npm install express

The next step is to create a file cars.js in the directory 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}/`);
});

In this file we describe the rules of our API.
The object will be a car with several properties (Manufacturer, model and color). The list will have only 3 objects.

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'}
];

Methods used on the server
GET(ALL) get the entire list of objects
GET (ID) get data of a specific object by ID
POST create new objects
PUT replace specific object by ID
DELETE delete specific object by ID
PATCH replace part of a property of a specific object by ID

By default the server will run on port 3000 http://localhost:3000/

Next you need to create index.html file in subdirectory 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>
        &nbsp.then(response => response.json())<br>
        &nbsp.then(data => console.log('GET full Car list:', data))<br>
        &nbsp.catch(error => console.error('Error:', error));<br>
	</p>
    </div>
</body>
</html>

The data from this file will be displayed on the main page.

The final server structure should look like this

API server

API server

We start the server with the command node cars.js and check by opening the link in the browser http://localhost:3000/

Home page

Home page

We are checking API for performance. Open the console in dev tools – F12copy the GET ALL request from the page and click Enter.

dev tools console

dev tools console

The result is that we received a list of cars that are stored on our server.

For the simplest understanding of the functionality, let's imagine that the server is a garage with cars and the methods GET – view/use the car, POST – buy a new car, DELETE – sell a car, PATCH – tuning/upgrading cars and PUT – get a replacement car.

2. Writing Auto Tests Playwright + TS

For the Playwright framework, you need to create a new directory, for example test_cars_api

  1. Open a command prompt and navigate to your working directory, or open the directory with a code editor (such as VSC).

  2. Install the Playwright framework by running the command npm init playwright@latest and selecting the necessary options (typescript language, test directory, etc.)

  3. Install additional libraries if necessary
    npm install typescript

  4. Create a new test file in the folder 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';
})

First, let's add the import of the required elements from playwright and the base URL.

In the settings file playwright.config.ts you can correct the config to run tests only in one browser, for example name: 'chromium', comment out the rest.

Test 1 – Get All check to get the full list.

We are sending GET request for the entire list, check that the status and number of objects in the list. To double-check, output the list to the console.

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

Variable response the value is assigned to the request get on the base URLwhich has already been created. Next, the status is checked, the value is 200 (ok). Then the variable cars the answer is assigned in json format. At the end it is checked that the received list contains 3 cars length = 3.

Let's run the test npx playwright test

As a result we see a list of cars with their properties, the test was passed successfully.

Test result 1

Test result 1

Test 2 – Get by ID. Obtaining data about a specific object.

We are sending GET request for the first object with id = 1, check that the status is 200 and that the result in the object properties corresponds to the expected.

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

In this test, in the GET request we use a modified base URL to retrieve data for a specific car from the list – variable carID. The last operation checks the received properties of the object with the expected ones.

We launch the second test by name npx playwright test -g "Get Car by ID"

Result Test 2.

Result Test 2.

Result – test passed successfully, data matches.

Test 3 – POST. Creating a new object.

We are sending POST request and create a new object, the 4th on our list.

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

In this test we declare a new variable newCar and with the help of POST request we transfer data to the server via the base URL. We check that the server returned us the status 201 (created) and compare the object data to make sure the data is correct.

Run the test by name npx playwright test -g "POST new Car"

Result - Test 3

Result – Test 3

Result: the test was successfully passed, new data was added to the server.
By sending a GET ALL request, the server will return an expanded list containing the new object.

Dev tools console

Dev tools console

Test 4 – PUT. Replacing an object.

We are sending PUT request and pass an object with 2 properties out of 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);
    })

In this test we use the variable carID so that the server understands which of the objects will be replaced. We declare a variable updatedCar, which has 2 properties out of 3 basic ones (*for example) and with the help of PUT request we transfer data to the server. We check that the server returned us the status 200 (ok) and compare the object data to make sure the data is correct.

Run the test by name npx playwright test -g "PUT existing car"

Result Test 4

Result Test 4

Result – the test is completed successfully. If we execute the GET ALL request, we will see that the brand property is missing in object #2.

Dev tools console

Dev tools console

Test 5 – PATCH. Updating an object property.

We are sending PATCH request and pass data to replace 1 of the object's properties.

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

In this test we use the variable carID so that the server understands in which of the objects the changes will be made. We declare a variable updatedCar, which has 1 property out of 3 basic ones and with the help of PATCH request we transfer data to the server. We check that the server returned us the status 200 (ok) and compare the object data to make sure the data is correct.

Run the test by name npx playwright test -g "PATCH property of existing Car"

Result Test 5

Result Test 5

Result – the test is completed successfully. If we execute the GET ALL request, we will see that object #3 has all 3 properties and only one was updated – color. Now we see a clear difference between PUT And PATCH.

Dev tools console

Dev tools console

Test 6 – DELETE. Deleting an object.

We are sending DELETE request and delete the object.

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

In this test we use the variable carID so that the server understands which of the objects should be deleted. We send a request DELETE and check that the status is 204(No content). The next operation is to send GET request for URL remote object and check that the status is 404(Not found).

Run the test by name npx playwright test -g "DELETE car by ID"

Result Test 6

Result Test 6

Result – the test was completed successfully. If we execute the GET ALL request, we will see that object #4 was removed from the list.

Dev tools console

Dev tools console

3. Negative scenarios and error analysis.

Test 7 – GET. Request for a non-existent object.

    test('Negative Get Car by ID', async ({request}) => {
        const carID = 4;
        const response = await request.get(`${baseURL}/${carID}`);
        expect(response.status()).toBe(404);
    })
})

By default the server contains 3 objects. Let's send GET request for a non-existent 4th object. As a result, we will get a status equal to 404. This test contains almost the same conditions as Test 6, but in order to make sure that the test works correctly, we will replace the value carID on 1.

Result – error the received status does not match the expected one. Since we replaced carID for an existing object the server returned us status 200.

Result Test 7.

Result Test 7.

Test 8 – PUT. Request to add a non-existent property.

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

For example, we want to change the 3rd object to add a property engineSince we do not have access to the documentation, we expect a positive result and status 200.

As a result, the test failed, and error status 501 was received instead of the expected 200.

Error Test 8

Error Test 8

Let's update the test taking into account the special validation case for the method PUTwhich turns out to be on the server. When sending the property engine we are waiting for the status 501with error description – 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');
    })

Result: test completed successfully.

Result Test 8

Result Test 8

Test 9 – POST. Special case: incorrect logic on the server.

For this test, you need to make changes on the server – make an error in processing the POST request. Let's swap brand And color.

    const newcar = {
        id: cars.length + 1,
        brand: req.body.color,
        model: req.body.model,
        color: req.body.brand,
    };

Restart the server. Launch the previously created Test 3 – POST new Car.
npx playwright test -g "POST new Car"

As a result, we see data inconsistencies when checking the properties of an object.

Result Test 9.

Result Test 9.

That's all. I hope the material was written as easy as possible to understand and repeat practical tasks.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *