Parallel end-to-end testing
End-to-end testing is a method to test the application fully from a user perspective in an automated way. This means that there is no separated module or mock during the test execution. The tests are running against a real application and database. Recently, Playwright has emerged as the de-facto end-to-end testing library. In my opinion, this is thanks to its ability to simulate the most popular browsers in parallel. Let’s dive into that.
I don’t want to bother anyone with the boring details of how to use Playwright. Let’s say that if you’ve ever worked with Jest, you’re already familiar with the syntax. This post will be about using test data in parallel within tests.
Navigate to "Between tests" Between tests
In my use case, I needed unique IDs with each test execution. It wouldn’t take so long for an average Joe to add a while
or for
loop to simply use incremented numbers as IDs. But I have better advice for you. JavaScript generators fit best for this task. Let’s see an example.
*generator(index: number) {
while (index < Number.MAX_SAFE_INTEGER) {
yield index++;
}
return -1;
}
const it = this.generator(info, 1);
it.next().value; // 1
it.next().value; // 2
it.next().value; // 3
Navigate to "Between workers" Between workers
Playwright uses Web Workers to run tests in parallel. Each worker has a separate context and browser. This means that they cannot communicate with each other, which is a huge deal when you want to ensure that tests are using unique IDs. Luckily, Playwright can assign unique IDs to workers (workerIndex
, parallelIndex
) and inject them into tests. This helps us further ensure unique IDs.
*generator(testInfo: TestInfo, index: number) {
const { workerIndex } = testInfo;
while (index < Number.MAX_SAFE_INTEGER) {
yield `${workerIndex + 1}.${index++}`;
}
return '';
}
const it = this.generator(info, 1);
it.next().value; // 1.1
it.next().value; // 1.2
it.next().value; // 1.3
Navigate to "Between projects" Between projects
Projects group multiple tests. Within a project, tests use the same configuration and browser by default. But what if you are running parallel browser end-to-end tests against a single database instance? Well, you will end up with the same ID sequence for each project, and they will conflict. We can leverage the configuration to our advantage. Let’s parameterize each config and inject them into the generator.
// playwright.config.js
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], projectId: '1' },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'], projectId: '2' },
},
],
});
// fixture.ts
export const test = base.extend<TestOptions>({
// Our generator fixture
idGenerator: [
async ({ projectId }, use) => {
await use(new IdGenerator(projectId, test.info().workerIndex));
},
{ scope: 'worker' },
],
});
// id-generator.ts
class IdGenerator {
readonly it;
readonly projectId;
readonly workerId;
current: string;
constructor(projectId: string, workerId: number) {
this.it = this.generator(1);
this.projectId = projectId;
this.workerId = workerId;
this.current = '';
}
*generator(index: number) {
while (index < Number.MAX_SAFE_INTEGER) {
yield `${this.projectId}.${this.workerIndex + 1}.${index++}`;
}
return '';
}
next() {
this.current = this.it.next().value;
return this.current;
}
}
Navigate to "Conclusion" Conclusion
Obviously, the generated ID is not the most secure and fault-tolerant. It’s the reader’s responsibility to align the algorithm accordingly. I hope you got the idea. Until next time.