๐งช Building Playwright Framework Step By Step - Implementing API Tests
๐ฏ 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",
"[email protected]",
"[email protected]",
"username@domain,com",
"username@[email protected]",
"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: '[email protected]',
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: '[email protected]',
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!
๐๐ป Thank you for reading! Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals ๐ who are passionate about mastering modern testing.
Join the community and get the latest articles and tips by signing up for the newsletter.
Top comments (0)