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:
// 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#
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#
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#
// 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#
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#
// 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#
// 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#
// 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#
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.
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.
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.