Technical exercises · Explain, implement and review

How to practice#

For each exercise, state the contract, write a small implementation, test a positive and negative/edge case, then discuss what it cannot prove. These examples are sandbox documentation, not an included payment backend. Use the separate local practice-workspace generator for deliberately broken starter code when you want fix-it tasks.

1 · Fix the missing await#

Problem: a helper reads status on a Promise. Ideal correction:

typescript
// Scoped test body: request and expect injected/imported.
const response = await request.get('/sandbox/payments/owned-payment');
expect(response.status()).toBe(200);
const body: unknown = await response.json();
expect(body).toEqual(expect.objectContaining({ id: 'owned-payment' }));

Explain why status() is synchronous but get/json are asynchronous. A rejected transport Promise and a returned HTTP 500 need different handling. Deep explanation.

2 · Deduplicate event IDs#

typescript
export function uniqueEventIds(ids: readonly string[]): string[] {
  return [...new Set(ids)];
}

Expected O(n) time/space under ordinary hash-table assumptions; preserves first encounter order. Empty input works. Exact ID matching is intentional. This local helper is not a persistent transactional deduplication mechanism across services/restarts. Business-level duplicates.

3 · Group compatible totals#

typescript
export function totalsByCurrency(rows: { currency: string; amountMinor: number }[]) {
  const totals = new Map<string, number>();
  for (const row of rows) {
    if (!Number.isSafeInteger(row.amountMinor)) throw new Error('Invalid amount');
    const next = (totals.get(row.currency) ?? 0) + row.amountMinor;
    if (!Number.isSafeInteger(next)) throw new Error('Total overflow');
    totals.set(row.currency, next);
  }
  return totals;
}

Expected O(n) time/O(k) extra space for k currencies. Clarify whether negatives represent refunds, which statuses belong in the total and currency normalization rules. Do not aggregate all currencies together. SQL counterpart.

4 · Independent versus dependent requests#

typescript
// Scoped test body: unrelated reads may overlap.
const [merchant, configuration] = await Promise.all([
  request.get('/sandbox/merchants/owned-merchant'),
  request.get('/sandbox/routing-rules/owned-version'),
]);
expect(merchant.status()).toBe(200);
expect(configuration.status()).toBe(200);

Follow a creation with a GET only after obtaining/validating its returned ID. Promise.all neither cancels other operations nor rolls back committed writes. Dependency graph.

5 · Parameterize amount boundaries#

typescript
import { test, expect } from '@playwright/test';
for (const item of [
  { amountMinor: -1, code: 'INVALID_AMOUNT' },
  { amountMinor: 0, code: 'INVALID_AMOUNT' },
  { amountMinor: 100001, code: 'LIMIT_EXCEEDED' },
]) {
  test(`rejects amount ${item.amountMinor}`, async ({ request }) => {
    const response = await request.post('/sandbox/payments', {
      data: { amountMinor: item.amountMinor, currency: 'GEL' },
    });
    expect(response.status()).toBe(422);
    expect(await response.json()).toMatchObject({ code: item.code });
  });
}

Hypothetical allowed range is 1..100000; also add positive cases at 1 and 100000 and verify rejected inputs have no payment effect. Actual creation prerequisites and cleanup belong in fixtures. Boundary design.

6 · Wait without sleeping#

typescript
// Scoped test body. Timeout is a documented test objective, not a guessed PSP SLA.
await expect(page.getByTestId('payment-status')).toHaveText('CAPTURED', { timeout: 12_000 });

This observes the UI. If final service state is the requirement, poll its API and handle terminal failures deliberately. A timed-out assertion needs the last observed state and correlated evidence. Do not repeat payment creation inside retry blocks. Polling.

7 · Two concurrent refunds#

typescript
// Scoped test body: a fixture created one isolated payment with 1000 minor units remaining.
const responses = await Promise.all([
  request.post(`/sandbox/payments/${payment.id}/refunds`, { data: { amountMinor: 700 } }),
  request.post(`/sandbox/payments/${payment.id}/refunds`, { data: { amountMinor: 700 } }),
]);
const statuses = responses.map(r => r.status()).sort((a, b) => a - b);
expect(statuses).toEqual([201, 409]); // hypothetical concurrency contract

The response combination alone is insufficient: after completion, verify refunded total is 700 and not 1400, correct currency, rejected operation has no effect, and audit identity is coherent. Some systems accept asynchronously instead; adapt expectations. Invariant.

8 · Detect a weak generated assertion#

typescript
// Weak: a heading can exist while payment processing is broken.
// await expect(page.getByRole('heading', { name: 'Payment' })).toBeVisible();
await expect(page.getByTestId('payment-status')).toHaveText('CAPTURED');
await expect(page.getByTestId('payment-amount')).toHaveText('GEL 12.50');

In a controlled fixture return PENDING or the wrong amount. The success test must fail for the intended reason. A UI mock checks assertion sensitivity/rendering, not actual PSP/ledger correctness. Mock boundary.

9 · Runtime parsing instead of assertion casting#

typescript
export function requiredId(value: unknown): string {
  if (value === null || typeof value !== 'object' || !('id' in value) ||
      typeof value.id !== 'string' || value.id.length === 0) throw new Error('Missing id');
  return value.id;
}

Tests: valid id, absent id, number id, empty string, null and array/object edge cases according to real schema. as {id:string} changes compiler belief without inspecting data. Runtime contract.

10 · Review a flawed payment test#

Hypothetical flawed design: shared merchant; create request not awaited; click then register listener; sleep five seconds; assert response.ok; retry the whole payment on timeout; no cleanup; upload full trace/token dump. Explain each failure mode before writing the correction.

Ideal design: dedicated account/payment identity; verified sandbox contracts; explicit dependency order; listener-before-trigger; exact status/runtime body; bounded final-state observation; idempotency/reconciliation policy; fixture/finally cleanup and orphan recovery; sanitized evidence with restricted retention. Full framework, diagnosis.

Interview delivery#

30 seconds: “I state assumptions, implement the smallest correct behavior and test boundaries. For async/payment code I explain what is awaited, what identity prevents collisions, and which evidence proves the effect. I name the contract-dependent parts rather than pretend every PSP behaves identically.”

90-second answer: thinking aloud during coding

I first paraphrase the task and clarify missing data types, allowed inputs and side effects. I write a small typed function or focused test, then walk a normal, empty/invalid and boundary case through it. I point out dependency order and failure propagation instead of filling the solution with generic retries. I discuss complexity for collection problems and precision for money. If a function is only a local guard, I explain why server concurrency still needs enforcement. I finish by showing how to run/typecheck the example and which integration assumptions require a real sandbox. That gives the interviewer evidence of reasoning as well as syntax.

Memory hook: assumptions, implementation, edge case, evidence, limitation. Reference: Playwright Test API. Continue to technical Q&A.

Quick practice#

How do you safely total amounts?
  • Filter the intended business states and group by compatible currency/scale
  • Use safe integer or specified decimal arithmetic and check overflow.

Pitfall: Summing currencies or floating values without a contract.

Full explanation.

Two 700-unit refunds race against 1000 remaining: test it.
  • Expect the agreed concurrency response/async outcome and verify total accepted effects do not exceed the remaining amount
  • Client-side eligibility checks alone cannot enforce it.

Pitfall: Only compare HTTP codes and ignore final ledger total.

Full explanation.

A success test only checks the Payment heading: improve it.
  • Assert the relevant status, amount/currency and owned entity, then inject a controlled wrong state to prove the assertion fails
  • Keep real integration evidence for provider/ledger behavior.

Pitfall: Counting assertions instead of checking their meaning.

Full explanation.

Mid-Level QA Automation Engineer — Payment Orchestration · CV refreshed 2026-10-16. Prepared from reviewed role-specific sources.