๐Ÿงช Building Playwright Framework Step By Step - Implementing API Tests

Ivan DavidovIvan Davidov
6 min read

๐ŸŽฏ Introduction

API testing in Playwright focuses on directly interacting with web applications at the API level, aiming to validate the functionality, reliability, efficiency, and security of RESTful services and endpoints! ๐Ÿš€ Unlike traditional browser-based tests, API testing with Playwright involves sending HTTP requests to the API and examining the responses, without rendering the graphical interface. This method provides a fast, efficient way to identify issues early in the development cycle, ensuring APIs adhere to their specifications and correctly integrate with front-end components and other services.

๐ŸŽฏ Why API Testing?: API tests are faster, more reliable, and provide better coverage than UI tests alone - they're the backbone of robust test automation!

๐Ÿ› ๏ธ Implement API Tests

๐Ÿ” Extend auth.setup.ts File

Incorporating API authentication and configuring the ACCESS_TOKEN environment variable ensures we possess a valid authentication token! ๐Ÿ”‘ This token can then be consistently appended to the headers of all necessary requests, streamlining the authentication process across our API interactions.

๐Ÿ’ก Authentication Strategy: Using API-based authentication setup is faster and more reliable than browser-based login flows.

import { test as setup, expect } from '../fixtures/pom/test-options';
import { User } from '../fixtures/api/types-guards';
import { UserSchema } from '../fixtures/api/schemas';

setup('auth user', async ({ apiRequest, homePage, navPage, page }) => {
    await setup.step('auth for user by API', async () => {
        const { status, body } = await apiRequest<User>({
            method: 'POST',
            url: 'api/users/login',
            baseUrl: process.env.API_URL,
            body: {
                user: {
                    email: process.env.EMAIL,
                    password: process.env.PASSWORD,
                },
            },
        });

        expect(status).toBe(200);
        expect(UserSchema.parse(body)).toBeTruthy();

        process.env['ACCESS_TOKEN'] = body.user.token;
    });

    await setup.step('create logged in user session', async () => {
        await homePage.navigateToHomePageGuest();

        await navPage.logIn(process.env.EMAIL!, process.env.PASSWORD!);

        await page.context().storageState({ path: '.auth/userSession.json' });
    });
});

๐Ÿ“Š Create Test Data for Invalid Credentials

Enables thorough testing of authentication, authorization, and data encryption! ๐Ÿ”’

๐Ÿ›ก๏ธ Security Testing: Testing with invalid data ensures your API properly handles malicious or malformed requests.

{
    "invalidEmails": [
        "plainaddress",
        "@missingusername.com",
        "username@.com",
        "username@domain..com",
        "username@domain,com",
        "username@domain@domain.com",
        "username@domain"
    ],
    "invalidPasswords": [
        "123",
        "7charac",
        "verylongpassword21cha",
        "",
        "     "
    ],
    "invalidUsernames": ["", "us", "verylongpassword21cha"]
}

๐Ÿ” Create 'authentication.spec.ts' File

Negative test cases aim to verify that the Application Under Test (AUT) correctly processes and handles invalid data inputs, ensuring robust error handling and system stability! โš ๏ธ

๐ŸŽฏ Testing Philosophy: Negative testing is just as important as positive testing - it ensures your API fails gracefully.

import { ErrorResponseSchema } from '../../fixtures/api/schemas';
import { ErrorResponse } from '../../fixtures/api/types-guards';
import { test, expect } from '../../fixtures/pom/test-options';
import invalidCredentials from '../../test-data/invalidCredentials.json';

test.describe('Verify API Validation for Log In / Sign Up', () => {
    test(
        'Verify API Validation for Log In',
        { tag: '@Api' },
        async ({ apiRequest }) => {
            const { status, body } = await apiRequest<ErrorResponse>({
                method: 'POST',
                url: 'api/users/login',
                baseUrl: process.env.API_URL,
                body: {
                    user: {
                        email: invalidCredentials.invalidEmails[0],
                        password: invalidCredentials.invalidPasswords[0],
                    },
                },
            });

            expect(status).toBe(403);
            expect(ErrorResponseSchema.parse(body)).toBeTruthy();
        }
    );

    test(
        'Verify API Validation for Sign Up',
        { tag: '@Api' },
        async ({ apiRequest }) => {
            await test.step('Verify API Validation for Invalid Email', async () => {
                for (const invalidEmail of invalidCredentials.invalidEmails) {
                    const { status, body } = await apiRequest<ErrorResponse>({
                        method: 'POST',
                        url: 'api/users',
                        baseUrl: process.env.API_URL,
                        body: {
                            user: {
                                email: invalidEmail,
                                password: '8charact',
                                username: 'testuser',
                            },
                        },
                    });

                    expect(status).toBe(422);
                    expect(ErrorResponseSchema.parse(body)).toBeTruthy();
                }
            });

            await test.step('Verify API Validation for Invalid Password', async () => {
                for (const invalidPassword of invalidCredentials.invalidPasswords) {
                    const { status, body } = await apiRequest<ErrorResponse>({
                        method: 'POST',
                        url: 'api/users',
                        baseUrl: process.env.API_URL,
                        body: {
                            user: {
                                email: 'validEmail@test.com',
                                password: invalidPassword,
                                username: 'testuser',
                            },
                        },
                    });

                    expect(status).toBe(422);
                    expect(ErrorResponseSchema.parse(body)).toBeTruthy();
                }
            });

            await test.step('Verify API Validation for Invalid Email', async () => {
                for (const invalidUsername of invalidCredentials.invalidUsernames) {
                    const { status, body } = await apiRequest<ErrorResponse>({
                        method: 'POST',
                        url: 'api/users',
                        baseUrl: process.env.API_URL,
                        body: {
                            user: {
                                email: 'validEmail@test.com',
                                password: '8charact',
                                username: invalidUsername,
                            },
                        },
                    });

                    expect(status).toBe(422);
                    expect(ErrorResponseSchema.parse(body)).toBeTruthy();
                }
            });
        }
    );
});

๐Ÿ“ Create 'article.spec.ts' File

The purpose of this test case is to validate the CRUD (Create, Read, Update, Delete) functionality, ensuring the system efficiently manages data operations! ๐Ÿ”„

๐Ÿ—๏ธ CRUD Testing: Comprehensive CRUD testing ensures your API can handle the full lifecycle of data operations.

import { ArticleResponseSchema } from '../../fixtures/api/schemas';
import { ArticleResponse } from '../../fixtures/api/types-guards';
import { test, expect } from '../../fixtures/pom/test-options';
import articleData from '../../test-data/articleData.json';

test.describe('Verify CRUD for Article', () => {
    test(
        'Verify Create/Read/Update/Delete an Article',
        { tag: '@Api' },
        async ({ apiRequest }) => {
            let articleId: string;

            await test.step('Verify Create an Article', async () => {
                const { status, body } = await apiRequest<ArticleResponse>({
                    method: 'POST',
                    url: 'api/articles/',
                    baseUrl: process.env.API_URL,
                    body: articleData.create,
                    headers: process.env.ACCESS_TOKEN,
                });
                expect(status).toBe(201);
                expect(ArticleResponseSchema.parse(body)).toBeTruthy();
                articleId = body.article.slug;
            });

            await test.step('Verify Read an Article', async () => {
                const { status, body } = await apiRequest<ArticleResponse>({
                    method: 'GET',
                    url: `api/articles/${articleId}`,
                    baseUrl: process.env.API_URL,
                });

                expect(status).toBe(200);
                expect(ArticleResponseSchema.parse(body)).toBeTruthy();
            });

            await test.step('Verify Update an Article', async () => {
                const { status, body } = await apiRequest<ArticleResponse>({
                    method: 'PUT',
                    url: `api/articles/${articleId}`,
                    baseUrl: process.env.API_URL,
                    body: articleData.update,
                    headers: process.env.ACCESS_TOKEN,
                });

                expect(status).toBe(200);
                expect(ArticleResponseSchema.parse(body)).toBeTruthy();
                expect(body.article.title).toBe(
                    articleData.update.article.title
                );
                articleId = body.article.slug;
            });

            await test.step('Verify Read an Article', async () => {
                const { status, body } = await apiRequest<ArticleResponse>({
                    method: 'GET',
                    url: `api/articles/${articleId}`,
                    baseUrl: process.env.API_URL,
                });

                expect(status).toBe(200);
                expect(ArticleResponseSchema.parse(body)).toBeTruthy();
                expect(body.article.title).toBe(
                    articleData.update.article.title
                );
            });

            await test.step('Verify Delete an Article', async () => {
                const { status, body } = await apiRequest({
                    method: 'DELETE',
                    url: `api/articles/${articleId}`,
                    baseUrl: process.env.API_URL,
                    headers: process.env.ACCESS_TOKEN,
                });

                expect(status).toBe(204);
            });

            await test.step('Verify the Article is deleted', async () => {
                const { status, body } = await apiRequest({
                    method: 'GET',
                    url: `api/articles/${articleId}`,
                    baseUrl: process.env.API_URL,
                });

                expect(status).toBe(404);
            });
        }
    );
});

๐Ÿงน Implement Tear Down Process

Tear down in the context of a test case refers to the process of cleaning up and restoring the testing environment to its original state after a test run is completed! ๐Ÿงฝ This step is crucial in ensuring that each test case starts with a consistent environment, preventing side effects from previous tests from influencing the outcome of subsequent ones.

๐ŸŽฏ Best Practice: Proper cleanup ensures test isolation and prevents flaky tests caused by leftover test data.

The tear down process can involve a variety of actions such as:

  • ๐Ÿ”Œ Closing connections

  • ๐Ÿ—‘๏ธ Deleting test data

  • โš™๏ธ Resetting system configurations

Effectively implementing the tear down phase helps maintain the integrity of the test suite, enabling accurate and reliable test results! โœจ

๐Ÿ’ก Fail-Safe Approach: The test covering article functionality includes a tear down step to delete the article created during the test. However, should any step following the article's creation fail, we've also implemented an API request to ensure the article is deleted, maintaining the integrity of the testing environment.

import { test, expect } from '../../fixtures/pom/test-options';
import { faker } from '@faker-js/faker';

test.describe('Verify Publish/Edit/Delete an Article', () => {
    const randomArticleTitle = faker.lorem.words(3);
    const randomArticleDescription = faker.lorem.sentence();
    const randomArticleBody = faker.lorem.paragraphs(2);
    const randomArticleTag = faker.lorem.word();
    let articleId: string;

    test.beforeEach(async ({ homePage }) => {
        await homePage.navigateToHomePageUser();
    });

    test(
        'Verify Publish/Edit/Delete an Article',
        { tag: '@Sanity' },
        async ({ navPage, articlePage, page }) => {
            await test.step('Verify Publish an Article', async () => {
                await navPage.newArticleButton.click();

                const response = await Promise.all([
                    articlePage.publishArticle(
                        randomArticleTitle,
                        randomArticleDescription,
                        randomArticleBody,
                        randomArticleTag
                    ),
                    page.waitForResponse(
                        (res) =>
                            res.url() ===
                                `${process.env.API_URL}api/articles/` &&
                            res.request().method() === 'POST'
                    ),
                ]);

                const responseBody = await response[1].json();
                articleId = responseBody.article.slug;
                console.log('articleId', articleId);
            });

            await test.step('Verify Edit an Article', async () => {
                await articlePage.navigateToEditArticlePage();

                await expect(articlePage.articleTitleInput).toHaveValue(
                    randomArticleTitle
                );

                await articlePage.editArticle(
                    `Updated ${randomArticleTitle}`,
                    `Updated ${randomArticleDescription}`,
                    `Updated ${randomArticleBody}`
                );
            });

            await test.step('Verify Delete an Article', async () => {
                await articlePage.deleteArticle();
            });
        }
    );

    test.afterAll(async ({ apiRequest }) => {
        const { status, body } = await apiRequest({
            method: 'DELETE',
            url: `api/articles/${articleId}`,
            baseUrl: process.env.API_URL,
            headers: process.env.ACCESS_TOKEN,
        });
    });
});

๐ŸŽฏ What's Next?

In the next article we will implement CI/CD Integration - automating our test execution pipeline! ๐Ÿš€

๐Ÿ’ฌ Community: Please feel free to initiate discussions on this topic, as every contribution has the potential to drive further refinement.


โœจ Ready to enhance your testing capabilities? Let's continue building this robust framework together!

0
Subscribe to my newsletter

Read articles from Ivan Davidov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ivan Davidov
Ivan Davidov

Automation QA Engineer, ISTQB CTFL, PSM I, helping teams improve the quality of the product they deliver to their customers. โ€ข Led the development of end-to-end (E2E) and API testing frameworks from scratch using Playwright and TypeScript, ensuring robust and scalable test automation solutions. โ€ข Integrated automated tests into CI/CD pipelines to enhance continuous integration and delivery. โ€ข Created comprehensive test strategies and plans to improve test coverage and effectiveness. โ€ข Designed performance testing frameworks using k6 to optimize system scalability and reliability. โ€ข Provided accurate project estimations for QA activities, aiding effective project planning. โ€ข Worked with development and product teams to align testing efforts with business and technical requirements. โ€ข Improved QA processes, tools, and methodologies for increased testing efficiency. โ€ข Domain experience: banking, pharmaceutical and civil engineering. Bringing over 3 year of experience in Software Engineering, 7 years of experience in Civil engineering project management, team leadership and project design, to the table, I champion a disciplined, results-driven approach, boasting a record of more than 35 successful projects.