From 8b57afc66480ef1adc8a61c4588ae21ab53c67d3 Mon Sep 17 00:00:00 2001 From: Nikola Shekerev Date: Tue, 15 Oct 2024 19:23:40 +0300 Subject: [PATCH] Add tests --- .gitignore | 2 + README - Tests.md | 21 ++++++++ package-lock.json | 77 ++++++++++++++++++++++++++- package.json | 7 ++- playwright.config.ts | 48 +++++++++++++++++ tests/demo.spec.ts | 120 +++++++++++++++++++++++++++++++++++++++++++ tests/schemas.ts | 12 +++++ tsconfig.json | 2 +- 8 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 README - Tests.md create mode 100644 playwright.config.ts create mode 100644 tests/demo.spec.ts create mode 100644 tests/schemas.ts diff --git a/.gitignore b/.gitignore index 7d2a08c..1b733b1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ dist/ node_modules/ data.json .idea/ +test-results/ +playwright-report/ \ No newline at end of file diff --git a/README - Tests.md b/README - Tests.md new file mode 100644 index 0000000..b0a74a0 --- /dev/null +++ b/README - Tests.md @@ -0,0 +1,21 @@ +# Introduction + +## Scripts + +Please check the setup and tests script in package.json + +## Parametrization + +This demo showcases test parametrization. Tests are basically functions and can have params. This allows us to have a significant volume of test cases covered by a compact volume of code bodies + +## Response Validation + +This demo showcases using schemas to validate response structures. In real production scenarios we have highly complex structures in responses. Schemas are a standardized and simplified way to validate them + +## The problem of Scale VS The problem of troubleshooting in QA Automation + +QA Automation has some very standard problems. One such problem is the problem of scale. It is trivial to manage a project with 50 e2e test cases, but 500 is hard and 5000 is a living nightmare. The most obvious way to manage scale is to reuse code. This demo showcases 2 strategies to do that - parametrization and schemas. + +Both strategies are completely unnecessary for this test, because the problem we are solving here is super simple. My solution is a showcase + +Troubleshooting failing tests is the most common activity in QA. The most common way to waste our budget is to have QA automation where processing test results (this includes re-running, troubleshooting) is a nightmare. A common criticism to strategies I showcase here is that they make troubleshooting harder. There is some truth here. As anything in programing, we are dancing between trade-offs. If this topic is interesting to you, we can dive quite deeper. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8c9ce44..ab8fc8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,26 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@playwright/test": "^1.42.1", "@types/node": "^20.14.10", - "typescript": "^5.5.3" + "typescript": "^5.5.3", + "zod": "^3.23.8" + } + }, + "node_modules/@playwright/test": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz", + "integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/@types/node": { @@ -22,6 +40,53 @@ "undici-types": "~5.26.4" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/typescript": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", @@ -40,6 +105,16 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 88262e0..536b0e5 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,16 @@ "main": "index.js", "scripts": { "start": "npx tsc && node dist/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "setup": "npm install && npx playwright install", + "test": "npx playwright test" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { + "@playwright/test": "^1.42.1", "@types/node": "^20.14.10", - "typescript": "^5.5.3" + "typescript": "^5.5.3", + "zod": "^3.23.8" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..0fc7ef0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,48 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['list'], ['html']], + // required because of mailslurp latency + timeout: 120000, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL, we can set per environment */ + baseURL: 'http://localhost:3000', + + /* Maximum timeout for individual interaction, in milliseconds */ + actionTimeout: 5 * 1000, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + expect: { + timeout: 10000, + }, + + projects: [ + { + name: 'api_tests', + }, + ], + +}); diff --git a/tests/demo.spec.ts b/tests/demo.spec.ts new file mode 100644 index 0000000..bb19912 --- /dev/null +++ b/tests/demo.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from '@playwright/test'; +import { UserSchema, UserListSchema } from './schemas'; + +test.describe('Basic api tests', () => { + + // The following are few custom validation functions + // They validate contents of responses + const validateUserList = (res: any) => { + expect(res).toMatchObject( + { users: [ { name: 'Alice' }, { name: 'Bob' } ] } + ); + expect(res.users.length).toBe(2); + } + + const validateUser = (res: any) => { + expect(res).toMatchObject( + { name: 'Bob' } + ); + } + + // The following is the data for all test cases + const TEST_CASES = [ + { + name: "Get All Users", + url: "/users", + method: "GET", + expectedStatus: 200, + schema: UserListSchema, + customValidation: validateUserList, + }, + { + name: "Get Specific User", + url: "/users/2", + method: "GET", + expectedStatus: 200, + schema: UserSchema, + customValidation: validateUser, + }, + { + name: "Get Nonexistent Endpoint", + url: "/nonexistent", + method: "GET", + expectedStatus: 404, + }, + { + name: "Get Nonexistent user", + url: "/users/22", + method: "GET", + expectedStatus: 404, + }, + { + name: "Create user - unsupported", + url: "/users", + method: "POST", + expectedStatus: 404, + }, + { + name: "Update user - unsupported", + url: "/users/2", + method: "PUT", + expectedStatus: 404, + }, + { + name: "DELETE user - unsupported", + url: "/users/2", + method: "DELETE", + expectedStatus: 404, + }, + { + name: "Get Health", + url: "/health", + method: "GET", + expectedStatus: 200, + expectedNonJsonText: "OK", + }, + ] + for (const params of TEST_CASES) { + test(`Scenario: ${params.name}`, async ({ request}) => { + // Send Request + let response; + switch (params.method) { + case 'GET': + response = await request.get(params.url); + break; + case 'POST': + response = await request.post(params.url); + break; + case 'PUT': + response = await request.put(params.url); + break; + case 'DELETE': + response = await request.delete(params.url); + break; + default: + throw new Error(`Unsupported HTTP method: ${params.method}`); + } + + // Check Status Code + expect(response.status()).toBe(params.expectedStatus); + + // Validate Schema, optional param + if (params.schema !== undefined) { + const res = await response.json(); + params.schema.parse(res); + } + + // Additional custom validation, optional param + if (params.customValidation !== undefined) { + const res = await response.json(); + params.customValidation(res); + } + + // Some endpoints return plain text, optional param + if (params.expectedNonJsonText !== undefined) { + const actualText = await response.text(); + expect(actualText).toBe(params.expectedNonJsonText); + } + }); + } +}); diff --git a/tests/schemas.ts b/tests/schemas.ts new file mode 100644 index 0000000..bb69684 --- /dev/null +++ b/tests/schemas.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +// User +export const UserSchema = z.object({ + id: z.string(), + name: z.string(), +}); + +// List of users +export const UserListSchema = z.object({ + users: z.array(UserSchema), +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e05745b..b2b28c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES6", "module": "commonjs", "outDir": "./dist", - "rootDir": "./src", + "rootDirs": ["./src", "./tests"], "strict": true, "esModuleInterop": true }