30 վայրկյանում
Թեստերը փոքր ծրագրեր են, որոնք ստուգում են, որ ձեր կոդն անում է այն, ինչ պետք է։ Unit թեստերը ստուգում են մեկ ֆունկցիա, feature կամ integration թեստերը՝ ամբողջ հարցումը իրական տվյալների բազայի դեմ, իսկ end-to-end թեստերը սեղմում-անցնում են իրական հավելվածով բրաուզերում։ Լավ թեստային հավաքածուն աշխատում է յուրաքանչյուր pull request-ի վրա և ռեգրեսիաները բռնում է մինչև հաճախորդները կնկատեն դրանք։
Ինչու եմ օգտագործում
- Թեստեր գրում եմ, որովհետև այն մասերը, որոնք շարժում են գումար, պահեստի մնացորդ և loyalty միավորներ, երբեք չպետք է լուռ կոտրվեն։
- API-ների համար հենվում եմ feature թեստերի վրա, որովհետև դրանք ստուգում են այն, ինչ կլիենտներն իրականում կանչում են, այլ ոչ թե ներքին մանրամասները։
- Թեստերը CI-ում գործարկում եմ յուրաքանչյուր pull request-ի վրա, որպեսզի թիմը և մեզ հետ աշխատող AI agent-ները արագ feedback ստանան։
- Հենց թեստերն են մեծ codebase-ի refactoring-ը դարձնում հնարավոր, այլ ոչ թե սարսափելի։
Ինչպես է աշխատում
01Թեստերի բուրգը
Մտածեք մեքենայի ստուգման մասին։ Յուրաքանչյուր դետալ ստուգում եք ստենդի վրա (unit թեստեր)՝ շատ, արագ, էժան։ Համակարգերը ստուգում եք միասին՝ շարժիչը փոխանցման տուփի հետ (integration և feature թեստեր)՝ ավելի քիչ, ավելի դանդաղ։ Հետո ամբողջ մեքենան տանում եք փորձնական երթի (end-to-end թեստեր)՝ քիչ, ամենադանդաղ, բայց իրականությանն ամենամոտ։ Բուրգն ասում է՝ ներքևում շատ unit թեստեր, integration թեստերի ամուր շերտ, վերևում E2E-ի բարակ շերտ։ Backend API-ների համար շատ թիմեր նախընտրում են «գավաթի» ձևը, որտեղ ամենամեծ շերտը feature թեստերն են, որոնք կանչում են իրական endpoint-ներ իրական թեստային բազայով, որովհետև բագերի մեծ մասը հենց այնտեղ է ապրում։ Ամեն դեպքում՝ արագ թեստերն աշխատում են անընդհատ, դանդաղները ծածկում են միայն կրիտիկական սցենարները։
- Unit. մեկ ֆունկցիա կամ class՝ առանց տվյալների բազայի կամ ցանցի։
- Feature / integration. HTTP հարցումը մտնում է, ստուգվում են տվյալների բազան և պատասխանը։
- E2E. իրական բրաուզեր կամ սարք, որն անցնում է իրական UI-ով և API-ով։
02PHPUnit և Pest Laravel-ի համար
Laravel-ը գալիս է PHPUnit-ով և Pest-ով (ավելի հաճելի syntax PHPUnit-ի վրա)։ Feature թեստերը HTTP հարցումներ են ուղարկում ձեր հավելվածին հիշողության մեջ և assert են անում պատասխանի, տվյալների բազայի և side effect-ների վրա։ Օգտագործեք RefreshDatabase trait-ը, որպեսզի յուրաքանչյուր թեստ սկսվի մաքուր վիճակից. թեստերը գործարկեք նույն database engine-ի դեմ, ինչ production-ում (MySQL կամ PostgreSQL), որովհետև SQLite-ը եզրային դեպքերում այլ կերպ է իրեն պահում։ Factory-ները մեկ տողով ստեղծում են իրատեսական մոդելներ՝ state-ներով տարբերակների համար, օրինակ՝ վճարված պատվեր կամ արգելափակված օգտատեր։ Laravel-ի fake-երը փոխարինում են արտաքին ազդեցությունները. Queue::fake, Mail::fake, Notification::fake, Event::fake և Http::fake թույլ են տալիս assert անել, որ ինչ-որ բան ուղարկվել է՝ առանց այն իրականում ուղարկելու։
it('lets a customer place an order', function () {
Queue::fake();
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 2500]);
$this->actingAs($user)
->postJson('/api/v1/orders', ['items' => [['product_id' => $product->id, 'qty' => 2]]])
->assertCreated()
->assertJsonPath('data.total', 5000);
$this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 5000]);
Queue::assertPushed(SendOrderConfirmation::class);
});
03Jest, Vitest և node:test TypeScript-ի համար
JavaScript-ի աշխարհում Vitest-ը ժամանակակից default-ն է Vite-ի վրա հիմնված և նոր TypeScript նախագծերի մեծ մասի համար՝ արագ, ESM-native և Jest-համատեղելի։ Jest-ը դեռ տարածված է, հատկապես React Native-ում և ավելի հին codebase-երում։ Node-ն ունի նաև ներկառուցված runner՝ node:test, ընդհանրապես առանց dependency-ների։ NestJS-ի համար օգտագործեք testing module-ը՝ հավելվածի մի հատված կառուցելու և provider-ները fake-երով փոխարինելու համար. API թեստերի համար supertest-ը հարցումներ է ուղարկում հավելվածին։ Մաքուր տրամաբանությունը (գնագոյացում, թույլտվություններ, ամսաթվերի կանոններ) ստուգեք բազմաթիվ փոքր unit թեստերով, իսկ endpoint-ները՝ իրական PostgreSQL-ի դեմ Docker-ում կամ Testcontainers-ով, այլ ոչ թե ORM-ը mock անելով։
import { describe, it, expect } from 'vitest';
import { applyPromotion } from './pricing';
describe('applyPromotion', () => {
it('never discounts below zero', () => {
expect(applyPromotion({ subtotal: 1000 }, { type: 'fixed', amount: 1500 }).total).toBe(0);
});
it('applies percentage to subtotal only, not delivery', () => {
const r = applyPromotion({ subtotal: 10_000, delivery: 500 }, { type: 'percent', amount: 10 });
expect(r.total).toBe(9_500);
});
});
04Playwright end-to-end թեստերի համար
Playwright-ը կառավարում է իրական Chromium, Firefox և WebKit բրաուզերներ։ Այն ավտոմատ սպասում է էլեմենտներին, ուստի թեստերը շատ ավելի քիչ են flaky, քան հին գործիքների դեպքում, և երբ ինչ-որ բան ձախողվում է, գրանցում է trace-եր՝ screenshot-ներով և ցանցային log-երով։ E2E թեստեր գրեք այն մի քանի սցենարների համար, որոնք գումար են բերում կամ կորցնում. մուտք, զամբյուղ ավելացնել և checkout, ադմինի հիմնական գործողություն։ Էլեմենտներն ընտրեք ըստ role-ի և տեսանելի տեքստի, այնպես, ինչպես օգտատերերն են գտնում դրանք, այլ ոչ թե CSS class-ներով։ Յուրաքանչյուր թեստին անհրաժեշտ տվյալները seed արեք API-ի կամ fixture-ի միջոցով, այլ ոչ թե setup էկրաններով սեղմելով, և հավաքածուն գործարկեք staging-ի նման միջավայրում՝ արտաքին ծառայությունները stub արված։
import { test, expect } from '@playwright/test';
test('guest can check out', async ({ page }) => {
await page.goto('/products/sample-product');
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('link', { name: 'Cart' }).click();
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Phone').fill('+10000000000');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByRole('heading', { name: 'Thank you' })).toBeVisible();
});
05Արտաքին ծառայությունների mock և fixture-ներ
Ձեր թեստերը երբեք չպետք է կանչեն իրական վճարային gateway, SMS պրովայդեր կամ ERP։ Փոխարինեք դրանք սահմանի վրա. յուրաքանչյուր ինտեգրացիա փաթեթավորեք ձեր սեփական client class-ի մեջ, իսկ թեստերում այն փոխարինեք fake-ով կամ stub արեք դրա HTTP կանչերը (Laravel-ում՝ Http::fake, Node-ում՝ MSW կամ nock)։ Իրատեսական պատասխանները, ներառյալ սխալներն ու timeout-ները, գրանցեք որպես fixture-ներ, որպեսզի ստուգեք նաև ձախողման ճանապարհները։ Mock մի արեք այն, ինչ ձերն է, օրինակ՝ ձեր սեփական service-ները կամ տվյալների բազան. չափից շատ mock-ը տալիս է թեստեր, որոնք անցնում են, մինչ production-ը կոտրվում է։ Factory-ները թեստային տվյալներ են կառուցում խելամիտ default-ներով, այնպես որ յուրաքանչյուր թեստ նշում է միայն այն, ինչ իր համար կարևոր է։ Թեստերը պահեք անկախ. ոչ մի ընդհանուր state, ոչ մի պարտադիր հերթականություն։
06Թեստավորում CI-ում և ինչը ստուգել առաջինը
Lint-ը, type check-երը և թեստերը գործարկեք յուրաքանչյուր pull request-ի վրա և արգելափակեք merge-ը, երբ դրանք ձախողվում են։ CI job-ում օգտագործեք իրական database service, թեստերը գործարկեք զուգահեռ և cache արեք dependency-ները, որպեսզի feedback-ը գա րոպեների ընթացքում։ Եթե E2E-ն դանդաղ է, գործարկեք այն main-ի վրա կամ գիշերը։ Իրական պրոդուկտում, որտեղ թեստեր չկան, մի ձգտեք coverage-ի թվերի։ Սկսեք այնտեղից, որտեղ բագն ամենաթանկն է նստում. գումար (checkout-ի ընդհանուր գումարներ, զեղչեր, վերադարձներ, վճարման callback-ներ), թույլտվություններ և tenant-ների մեկուսացում, ինչպես նաև state machine-ներ, օրինակ՝ պատվերի կարգավիճակը։ Այնուհետև յուրաքանչյուր շտկված բագի համար ավելացրեք թեստ, որպեսզի այն երբեք չվերադառնա։ Coverage-ը բնականորեն աճում է այնտեղ, որտեղ դա կարևոր է։
# .github/workflows/test.yml (excerpt)
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: app_test }
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 24, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck && pnpm lint && pnpm test
Օգտագործեք, երբ
- Ցանկացած կոդ, որը հաշվարկում է գումար, պահեստի մնացորդ, միավորներ կամ թույլտվություններ։
- Refactoring-ից առաջ՝ ներկայիս վարքագիծը ամրագրելու համար։
- Յուրաքանչյուր բագի շտկման համար՝ որպես regression թեստ։
- Ընդհանուր package-ներ և API-ներ, որոնք օգտագործում են մի քանի հավելվածներ։
Մի՛ օգտագործեք, երբ
- Մեկանգամյա prototype-ներ, որոնց կոդը հաջորդ շաբաթ կջնջվի։
- Framework-ի ներքին մասերի կամ տրիվիալ getter-ների թեստավորում՝ միայն coverage-ը բարձրացնելու համար։
- Հսկայական snapshot թեստեր, որոնք ոչ ոք չի կարդում և որոնք կուրորեն թարմացվում են։
Այլընտրանքներ
- Static typing և linting
TypeScript strict-ը և PHPStan-ը բռնում են բագերի մի ամբողջ դաս՝ դեռ մինչև որևէ թեստի գործարկումը։
- Contract թեստեր
Ստուգում են, որ API-ի producer-ներն ու consumer-ները համաձայն են տվյալների ձևի շուրջ՝ առանց լիարժեք E2E-ի։
- Manual QA և exploratory testing
Դեռ արժեքավոր են UX-ի և այն եզրային դեպքերի համար, որոնք ավտոմատացումը բաց է թողնում։
- Մոնիթորինգ production-ում
Error tracking-ը և alert-ները բռնում են այն, ինչ թեստերը չէին կարող կանխատեսել։
Լավագույն պրակտիկաներ
- Վարքագիծը ստուգեք public ինտերֆեյսների միջով, այլ ոչ թե private իրականացման մանրամասներով։
- API-ների համար նախընտրեք feature թեստեր իրական տվյալների բազայով։
- Արտաքին ծառայությունները fake արեք սահմանի վրա. մի mock արեք ձեր սեփական կոդը։
- Օգտագործեք factory-ներ, որպեսզի յուրաքանչյուր թեստ ցույց տա միայն իր համար կարևոր տվյալները։
- Յուրաքանչյուր բագ շտկելուց առաջ գրեք ձախողվող թեստ։
- Թեստերը գործարկեք յուրաքանչյուր pull request-ի վրա և հավաքածուն պահեք արագ։
- E2E թեստերը պահեք քիչ՝ կենտրոնացած եկամտի համար կրիտիկական սցենարների վրա։
Տարածված սխալներ
- Հետապնդել 100% coverage, մինչդեռ checkout-ի հոսքը ոչ մի թեստ չունի։
- Mock անել տվյալների բազան կամ ORM-ը, որ թեստերն անցնեն, բայց իրական query-ները ձախողվեն։
- Flaky թեստեր, որոնք կախված են ժամանակից, հերթականությունից կամ ցանցից, և թիմեր, որոնք սովորում են անտեսել կարմիր build-երը։
- Թեստավորել SQLite-ի վրա, մինչդեռ production-ը աշխատում է MySQL-ով կամ PostgreSQL-ով։
- Իրական API key-եր կամ կանչեր live վճարային պրովայդերներին թեստերում։
Խորը վերլուծություն
07Արագ, մեկուսացված տվյալների բազայի թեստեր
Տվյալների բազայի թեստերը backend-ի ամենաարժեքավոր թեստերն են և ամենահեշտը դանդաղեցնելու համար։ Մեկուսացման ռազմավարությունները՝ ամենաարագից սկսած. յուրաքանչյուր թեստ փաթաթել տրանզակցիայի մեջ և հետ գլորել (Laravel-ի RefreshDatabase-ը դա անում է՝ մեկ գործարկման ընթացքում մեկ անգամ միգրացիա անելուց հետո, իսկ LazilyRefreshDatabase-ը բաց է թողնում միգրացիան, եթե թեստը բազային ընդհանրապես չի դիպչում). թեստերի միջև մաքրել աղյուսակները (truncate), երբ թեստավորվող կոդը բացում է իր սեփական տրանզակցիաները կամ կապերը. կամ ամբողջությամբ վերստեղծել բազան, ինչը ամենադանդաղն է։ Գործարկեք զուգահեռ. php artisan test --parallel-ը յուրաքանչյուր պրոցեսի տալիս է սուֆիքսով սեփական բազա, որպեսզի թեստերը չբախվեն։ Node-ում Testcontainers-ը յուրաքանչյուր գործարկման համար Docker-ում բարձրացնում է իրական PostgreSQL, այնպես որ CI-ն ու նոութբուքերը օգտագործում են նույն շարժիչն ու տարբերակը, ինչ production-ը։ Արագության հնարքներ՝ CI-ում բազան պահեք tmpfs-ի վրա, PostgreSQL-ում մեկ անգամ միգրացիա արեք և կլոնավորեք template բազայից, և երբեք sleep մի օգտագործեք թեստերում։
import { before, after } from 'node:test';
import { execSync } from 'node:child_process';
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
let pg: StartedPostgreSqlContainer;
before(async () => {
pg = await new PostgreSqlContainer('postgres:17').start();
process.env.DATABASE_URL = pg.getConnectionUri();
execSync('npx prisma migrate deploy', { stdio: 'inherit' });
});
after(async () => {
await pg.stop();
});
08Օրինակներից այն կողմ՝ property-based և mutation testing
Օրինակային թեստերը ստուգում են այն դեպքերը, որոնց մասին դուք մտածել եք։ Property-based testing-ը կանոնները ստուգում է հարյուրավոր գեներացված մուտքերի վրա. «1,000-ի վրա 10% զեղչը տալիս է 900»-ի փոխարեն դուք ձևակերպում եք՝ «ցանկացած ենթագումարի և 0-ից 100 ցանկացած տոկոսի դեպքում ընդհանուր գումարը երբեք բացասական չէ և երբեք չի գերազանցում ենթագումարը»։ TypeScript-ի համար fast-check-ի նման գրադարանները գեներացնում են մուտքեր և, երբ դեպքը ձախողվում է, այն փոքրացնում են մինչև ամենափոքր ձախողվող օրինակը։ Այն փայլում է գնագոյացման, կլորացման, parsing-ի և ամսաթվերի տրամաբանության համար։ Mutation testing-ը չափում է, թե որքան լավն են ձեր թեստերը, այլ ոչ թե որքան կոդ են դրանք գործարկում։ PHP-ի համար Infection-ը և JavaScript-ի համար Stryker-ը փոքր փոփոխություններ են անում ձեր կոդում, օրինակ՝ >=-ը դարձնում են >, և նորից գործարկում թեստերը։ Եթե թեստերը դեռ անցնում են, այդ մուտանտը ողջ է մնացել, ինչը ցույց է տալիս, որ assertion է պակասում։ Coverage-ն ասում է, որ տողը գործարկվել է, իսկ mutation score-ը՝ որ թեստը կնկատեր, եթե այն սխալ լիներ։
import fc from 'fast-check';
import { test, expect } from 'vitest';
import { applyPromotion } from './pricing';
test('percentage promotions stay within 0 and the subtotal', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 10_000_000 }),
fc.integer({ min: 0, max: 100 }),
(subtotal, percent) => {
const { total } = applyPromotion({ subtotal }, { type: 'percent', amount: percent });
expect(total).toBeGreaterThanOrEqual(0);
expect(total).toBeLessThanOrEqual(subtotal);
},
),
);
});
09Ճարտարապետական և պայմանագրային (contract) թեստեր
Որոշ կանոններ վարքագծի մասին չեն, այլ կառուցվածքի, և թեստերը կարող են պաշտպանել նաև դրանք։ Pest-ի architecture testing-ը թույլ է տալիս պնդել, որ controller-ները երբեք չեն օգտագործում DB facade-ը, որ domain շերտը երբեք չի ներմուծում HTTP դասեր, կամ որ debug ֆունկցիաները, օրինակ՝ dd()-ն, չեն հասնում production։ Այն նաև ունի պատրաստի preset-ներ տարածված կանոնների համար։ Հավելվածների և ծառայությունների միջև contract թեստերը բռնում են սխալների ամենաթանկ դասը. մի կողմը փոխում է դաշտ, որից կախված է մյուսը։ Consumer-driven contract-ների դեպքում (ամենահայտնի գործիքը Pact-ն է) սպառողը գրանցում է, թե ինչ է սպասում մատակարարից, իսկ մատակարարի CI-ն ստուգում է, որ այն դեռ բավարարում է բոլոր սպառողներին։ Ավելի թեթև տարբերակ է feature թեստերում իրական API պատասխանները ձեր OpenAPI սխեմայով վավերացնելը։ Երկուսն էլ շատ ավելի արագ և ճշգրիտ են, քան լիարժեք end-to-end թեստերը՝ ապացուցելու համար, որ երկու համակարգերը դեռ համաձայն են միմյանց հետ։
// tests/ArchTest.php (Pest)
arch()->preset()->php(); // e.g. no dd(), dump() or var_dump()
arch()->preset()->security(); // flags eval(), md5(), sha1() and similar
arch('controllers do not query the database directly')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Support\Facades\DB');
arch('domain does not depend on HTTP')
->expect('App\Domain')
->not->toUse('Illuminate\Http');
10Ժամանակի, կրկնությունների և զուգահեռության թեստավորում
Production հասնող սխալները հաճախ ժամանակի և կրկնության մասին են. բաժանորդագրություն, որը մեկ վայրկյան շուտ է ավարտվում, job, որը retry-ի ժամանակ կրկնակի է ուղարկում, երկու հարցում, որոնք պահեստից ավելին են վաճառում։ Թեստավորեք դրանք դիտավորյալ։ Սառեցրեք կամ տեղափոխեք ժամանակը՝ սպասելու փոխարեն. Laravel-ի travelTo() և travel(), Carbon::setTestNow(), կամ Vitest-ում vi.useFakeTimers() և vi.setSystemTime()։ Թեստավորեք սահմանները՝ վերջնաժամկետից մեկ վայրկյան առաջ և հետո, ինչպես նաև ժամային գոտիները կեսգիշերին և ամառային ժամանակի անցումների շուրջ։ Idempotency-ի համար նույն job-ը կամ webhook-ը գործարկեք երկու անգամ և պնդեք, որ էֆեկտը մեկն է։ Race condition-ների դեպքում mock-երով unit թեստը ոչինչ չի ապացուցում. իրական զուգահեռ հարցումներ ուղարկեք իրական բազայի դեմ, օրինակ՝ Promise.all-ով integration թեստում կամ կարճ k6 սկրիպտով, և պնդեք, որ ինվարիանտը, օրինակ՝ պահեստի մնացորդը երբեք զրոյից ցածր չէ, դեռ պահպանվում է։
it('expires a subscription exactly at its end time', function () {
$sub = Subscription::factory()->create(['ends_at' => '2026-03-01 00:00:00']);
$this->travelTo('2026-02-28 23:59:59');
$this->artisan('subscriptions:expire')->assertSuccessful();
expect($sub->fresh()->status)->toBe(SubscriptionStatus::Active);
$this->travelTo('2026-03-01 00:00:01');
$this->artisan('subscriptions:expire')->assertSuccessful();
expect($sub->fresh()->status)->toBe(SubscriptionStatus::Expired);
});
it('creates one invoice even if the job runs twice', function () {
$order = Order::factory()->paid()->create();
GenerateInvoice::dispatchSync($order->id);
GenerateInvoice::dispatchSync($order->id);
expect(Invoice::where('order_id', $order->id)->count())->toBe(1);
});
Իրական իրավիճակներ
CI pipeline-ը տևում է 35 րոպե
Խնդիր
Backend-ի թեստային հավաքածուն աճել է մինչև մի քանի հազար թեստ։ Pull request-ները կես ժամից ավելի են սպասում CI-ին, ուստի ծրագրավորողները փոփոխությունները խմբավորում են և ավելի հազվադեպ են merge անում։
Ինչպես լուծել
Նախ պրոֆայլ արեք. Pest-ը և PHPUnit-ը կարող են ցույց տալ ամենադանդաղ թեստերը, և սովորաբար մի քանիսն են գերակշռում։ Հեռացրեք sleep-երն ու իրական ցանցային կանչերը և ավելացրեք Http::preventStrayRequests(), որպեսզի ցանկացած չկեղծված HTTP կանչ բարձրաձայն ձախողվի։ Անցեք տրանզակցիաների վրա հիմնված refresh-ի և lazy refresh-ի, գործարկեք --parallel-ով և CI-ի բազան դրեք tmpfs-ի վրա։ Pipeline-ը բաժանեք զուգահեռ job-երի՝ static analysis, unit և feature թեստեր, իսկ մեծ հավաքածուները shard արեք մեքենաների միջև Pest-ի կամ Playwright-ի sharding-ով։ Cache արեք Composer-ի և pnpm-ի կախվածությունները։ Լիարժեք E2E գործարկումները տեղափոխեք merge queue կամ գիշերային գործարկում՝ յուրաքանչյուր pull request-ի վրա թողնելով փոքր smoke հավաքածու։ Նպատակը տասը րոպեից պակասն է։
85% coverage, և այնուամենայնիվ գնագոյացման սխալ production-ում
Խնդիր
Զեղչերի մի համակցություն մի ամբողջ հանգստյան օր բացասական ընդհանուր գումարներ էր տալիս։ Գնագոյացման մոդուլն ուներ բարձր coverage, և բոլոր թեստերը կանաչ էին։
Ինչպես լուծել
Coverage-ն ապացուցեց, որ կոդը գործարկվել է, ոչ թե որ արդյունքները ստուգվել են։ Նախ գրեք regression թեստ հենց այդ համակցության համար։ Այնուհետև ավելացրեք property-based թեստեր գնագոյացման ինվարիանտների համար. ընդհանուր գումարը երբեք բացասական չէ, երբեք չի գերազանցում ենթագումարը, և զեղչերը երբեք չեն կուտակվում կանոններից ավելի։ Գնագոյացման մոդուլի վրա գործարկեք mutation testing Infection-ով կամ Stryker-ով և ողջ մնացած մուտանտները վերացրեք՝ իրական assertion-ներ ավելացնելով, հաճախ սահմանային արժեքների վրա, ինչպիսիք են զրոն, ճշգրիտ շեմերը և 100%-ը։ Գնագոյացումը դարձրեք մաքուր ֆունկցիա value object-ների վրա, որպեսզի այս թեստերը արագ մնան։ Վերջում կոդում ավելացրեք պաշտպանություն, որը մերժում է բացասական գումարը և ահազանգ է տալիս, որովհետև թեստերը նվազեցնում են ռիսկը, բայց երբեք չեն վերացնում այն։
Մոբայլ հավելվածը կոտրվեց «անվնաս» API փոփոխությունից հետո
Խնդիր
Backend ծրագրավորողը վերանվանեց մի դաշտ և մեկ այլը դարձրեց nullable։ Backend-ի բոլոր թեստերն անցան, բայց մոբայլ հավելվածը բոլոր օգտատերերի մոտ crash էր անում պատվերի էկրանին։
Ինչպես լուծել
Ոչինչ չէր ստուգում երկու հավելվածների միջև պայմանագիրը։ Կարճաժամկետ՝ հին դաշտը վերադարձրեք նորի կողքին և թողարկեք ուղղում։ Երկարաժամկետ՝ պայմանագիրը դարձրեք գործարկելի. գեներացրեք OpenAPI փաստաթուղթ կոդից կամ պահպանեք այն ձեռքով և feature թեստերում իրական պատասխանները վավերացրեք դրանով, որպեսզի վերանվանված կամ նոր nullable դարձած դաշտը ձախողի CI-ը։ Այն հավելվածների համար, որոնք նույնպես ձերն են, դիտարկեք consumer-driven contract թեստեր Pact-ով, որպեսզի մոբայլ թիմի սպասելիքները ստուգվեն backend-ի յուրաքանչյուր build-ի ժամանակ։ Code review-ում ավելացրեք կանոն, որ պատասխանների փոփոխությունները միայն ավելացնող են, իսկ հեռացումներն անցնում են տարբերակավորված deprecation-ով։
Արագ ուղեցույց
- Laravel թեստերը գործարկել զուգահեռ
php artisan test --parallel- Գործարկել մեկ Pest թեստ ըստ անվան
./vendor/bin/pest --filter="places an order"- Ձախողել CI-ը coverage-ի շեմից ցածր
./vendor/bin/pest --coverage --min=80- Թեստերում արգելափակել ցանկացած չկեղծված HTTP կանչ
Http::preventStrayRequests();- Vitest մեկ անգամ՝ coverage-ով
npx vitest run --coverage- Playwright-ը բաժանել չորս CI մեքենայի միջև
npx playwright test --shard=1/4- Բացել Playwright-ի ձախողման trace-ը
npx playwright show-trace trace.zip- Ձայնագրել Playwright թեստ՝ սեղմելով
npx playwright codegen https://staging.example.com- Mutation testing PHP-ի համար
vendor/bin/infection --min-msi=70 --threads=4- Mutation testing JS-ի և TS-ի համար
npx stryker run
Իմ իրական նախագծերում
- Կենդանիների ապրանքների պլատֆորմը, որը ղեկավարում եմ, ունի 95 backend թեստային ֆայլ, որոնք ծածկում են նրա Laravel service-ներն ու endpoint-ները։
- Մարզասրահների ցանցի CRM-ը, որը կառուցել եմ գրեթե մենակ, ունի 31 թեստային ֆայլ։
- Multi-tenant POS SaaS-ը, որը համատեղ ղեկավարում եմ, ունի մոտ 270 թեստային ֆայլ node:test-ի վրա, և յուրաքանչյուր module-ի թեստում կա «սխալ tenant» դեպք։
- Սուրճի հավելվածում post-edit hook-ը գործարկում է type check-եր, lint և locale-ների համապատասխանության ստուգում՝ գրանցված baseline-ի դեմ, իսկ UI աշխատանքի համար օգտագործում եմ բրաուզերային ստուգման քայլ 390, 768 և 1920 լայնություններում։
Հարցեր և պատասխաններ
Q1Ո՞րն է տարբերությունը unit, integration և end-to-end թեստերի միջև։
Unit թեստը ստուգում է մեկ փոքր մաս՝ ֆունկցիա կամ class, մեկուսացված, առանց տվյալների բազայի կամ ցանցի. այն աշխատում է միլիվայրկյանների ընթացքում։ Integration կամ feature թեստը ստուգում է մի քանի մաս միասին, օրինակ՝ HTTP հարցում, որն անցնում է routing-ով, validation-ով, service-ներով և իրական թեստային բազայով՝ assert անելով պատասխանի և պահված տվյալների վրա։ End-to-end թեստը գործարկում է իրական հավելվածը այնպես, ինչպես օգտատերը կաներ՝ բրաուզերում կամ սարքի վրա, frontend-ով և backend-ով։ Վերև շարժվելիս թեստերը դառնում են ավելի իրատեսական, բայց ավելի դանդաղ և ավելի փխրուն, ուստի ուզում եք շատ unit թեստեր, ամուր integration շերտ և քիչ E2E թեստեր։
Q2Ի՞նչը կստուգեիք առաջինը այն պրոդուկտում, որտեղ թեստեր չկան։
Այն, ինչն ամենաթանկն է նստում, երբ կոտրվում է։ Commerce-ում դա գումարն է՝ զամբյուղի ընդհանուր գումարներ, զեղչեր և ակցիաների համակցում, հարկեր, վերադարձներ և վճարման callback-ներ։ Հետո անվտանգությունը՝ նույնականացում, թույլտվություններ և tenant-ների մեկուսացում, ներառյալ թեստ, որ մեկ tenant-ը չի կարող տեսնել մյուսի տվյալները։ Այնուհետև state machine-ները, օրինակ՝ պատվերի կարգավիճակի անցումները և բաժանորդագրության ժամկետի ավարտը։ Դրանց համար գրում եմ feature թեստեր API-ի մակարդակում, որովհետև դրանք յուրաքանչյուր թեստի հաշվով տալիս են ամենաշատ պաշտպանությունը և դիմանում են refactoring-ին։ Այդ պահից սկսած յուրաքանչյուր բագի շտկում գալիս է regression թեստով։ Coverage-ն աճում է այնտեղ, որտեղ ռիսկն է, ոչ թե հավասարաչափ։
Q3Ե՞րբ պետք է mock անել, և ե՞րբ՝ ոչ։
Mock արեք այն, ինչ ձերը չէ և դանդաղ է, թանկ կամ անկանխատեսելի. վճարային gateway-ներ, SMS և email պրովայդերներ, ERP-ներ, ժամացույցը, պատահականությունը։ Յուրաքանչյուրը փաթեթավորեք ձեր սեփական adapter-ի մեջ և fake արեք այդ սահմանի վրա՝ ներառյալ սխալի և timeout-ի պատասխանները։ Մի mock արեք այն, ինչ ձերն է, օրինակ՝ ձեր service-ները կամ տվյալների բազան. թեստը, որը mock է անում ORM-ը, ապացուցում է միայն, որ դուք կանչել եք մեթոդը, ոչ թե որ query-ն աշխատում է։ Տվյալների բազայի համար օգտագործեք իրական engine Docker-ում՝ transaction-ներով կամ refresh-ով թեստերի միջև։ Չափից շատ mock-ը տալիս է կանաչ թեստեր և կոտրված production։
Q4Ինչպե՞ս եք վարվում flaky թեստերի հետ։
Վերաբերվում եմ դրանց որպես բագերի, որովհետև թիմը, որը սովորում է անտեսել կարմիր build-երը, դադարում է վստահել թեստային հավաքածուին։ Տարածված պատճառներն են թեստերի միջև ընդհանուր state-ը, գործարկման հերթականությունից կախվածությունը, իրական ժամանակն ու ժամային գոտիները, պատահականությունը, ցանցային կանչերը, իսկ E2E-ում՝ պայմանների փոխարեն timeout-ների սպասելը։ Լուծումներ՝ տվյալները մեկուսացնել յուրաքանչյուր թեստի համար, ժամանակը «սառեցնել», պատահականությանը seed տալ, արտաքին կանչերը fake անել և օգտագործել Playwright-ի ավտոմատ սպասող assertion-ները։ Flaky թեստը կարճ ժամանակով դրեք quarantine-ի՝ պատասխանատուով և ժամկետով, այլ ոչ թե թողեք, որ այն պատահականորեն ձախողվի։ Playwright-ի trace-երը և CI artifact-ները սովորաբար արագ ցույց են տալիս պատճառը։
Q5Pest, թե՞ PHPUnit, Jest, թե՞ Vitest։
Pest-ն աշխատում է PHPUnit-ի վրա, ուստի դա syntax-ի հարց է. Pest-ն ավելի հակիրճ և ընթեռնելի է և տարածված default է նոր Laravel նախագծերում, իսկ PHPUnit class-ները նույնպես լավ են և լիովին աջակցվում են։ Երկուսը մեկ նախագծում խառնելը աշխատում է։ TypeScript-ի համար Vitest-ը իմ default-ն է նոր նախագծերում՝ արագ, ESM-native, աշխատում է Vite-ի հետ և առաջարկում Jest-համատեղելի API։ Jest-ը շարունակում է տարածված մնալ React Native-ում և ավելի հին codebase-երում, ուստի աշխատող հավաքածուն միգրացնելու պատճառ չկա։ Node backend-ների համար ներկառուցված node:test runner-ը ամուր տարբերակ է՝ առանց dependency-ների։ Գործիքն ավելի քիչ է կարևոր, քան իմաստալից թեստեր գրելը։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս եք մեծ թեստային հավաքածուն արագ պահում, երբ codebase-ը աճում է։
Հավաքածուի արագությունը դիտարկեք որպես պրոդուկտային մետրիկա՝ բյուջեով, ասենք՝ CI-ում տասը րոպեից պակաս։ Կառուցվածքային առումով տրամաբանության մեծ մասը պահեք մաքուր ֆունկցիաներում, որոնք թեստավորվում են արագ unit թեստերով, իսկ endpoint-ների համար օգտագործեք feature թեստեր իրական բազայով՝ մեկուսացված տրանզակցիաներով, ոչ թե կրկին միգրացիայով։ Լոկալ գործարկեք զուգահեռ, իսկ CI-ում shard արեք մեքենաների միջև՝ կախվածությունների cache-ով և tmpfs բազայով։ Հեռացրեք դանդաղ օրինաչափությունները. sleep-եր, իրական ցանցային կանչեր, հսկայական factory-ներ, որոնք ամբողջ օբյեկտային գրաֆ են կառուցում, երբ բավական է երկու մոդել։ Ամեն շաբաթ հետևեք ամենադանդաղ թեստերին և ուղղեք գլխավոր խախտողներին։ Monorepo-ում pull request-ների վրա գործարկեք միայն ազդված թեստերը, բայց merge-ից առաջ միշտ՝ ամբողջ հավաքածուն։ Փոխզիջումը թեստային ենթակառուցվածքի մեջ ներդրումն է, որը վերադառնում է նրանով, թե որքան հաճախ են մարդիկ գործարկում թեստերը։
S2Թեստավորման ի՞նչ ռազմավարություն եք կիրառում մի քանի հավելվածներից և ծառայություններից բաղկացած համակարգի համար։
Յուրաքանչյուր բաղադրիչ ինքն իրեն մանրակրկիտ թեստավորում է մեկուսացված, իսկ միացման կետերը ստանում են հատուկ, էժան ստուգումներ։ Ծառայության ներսում՝ unit թեստեր domain տրամաբանության համար և feature թեստեր իրական բազայի դեմ, իսկ արտաքին համակարգերը կեղծվում են adapter-ի մակարդակում։ Բաղադրիչների միջև՝ contract թեստեր՝ կա՛մ consumer-driven՝ Pact-ով, կա՛մ OpenAPI-ով սխեմայի վավերացում, որպեսզի մատակարարները կոտրող փոփոխությունների մասին իմանան իրենց սեփական CI-ում։ Ընդհանուր package-ներն ունեն իրենց թեստերը և semantic տարբերակներ կամ monorepo-ում ազդված գործարկումներ։ Այնուհետև բարակ end-to-end շերտը staging միջավայրում ծածկում է եկամտի համար կրիտիկական մի քանի սցենար։ Բազմաթիվ ծառայություններով լիարժեք end-to-end հավաքածուները դանդաղ են և flaky, ուստի չեն կարող լինել հիմնական անվտանգության ցանցը։ Production մոնիթորինգը, synthetic ստուգումները և feature flag-երը փակում են մնացած բացը։
S3Coverage-ի թիրախը լա՞վ գաղափար է։
Որպես նվազագույն շեմ, որը կանխում է վատթարացումը՝ այո. որպես նպատակ՝ ոչ։ Թիմը, որին ասել են հասնել 90%-ի, թեստեր է գրում getter-ների և framework-ի կապող կոդի համար, երբեմն էլ թեստեր առանց assertion-ների, որովհետև դա թիվը շարժելու ամենաէժան ձևն է։ Մինչդեռ ռիսկային ուղիները՝ վերադարձումներ, զուգահեռություն, թույլտվություններ, կարող են թույլ ստուգված մնալ։ Ես նախընտրում եմ ռիսկի վրա հիմնված թիրախներ. բարձր սպասելիքներ գնագոյացման, վճարումների, auth-ի և tenant-ների մեկուսացման համար, ավելի թեթևը՝ ադմին էկրանների համար։ Coverage-ի հաշվետվություններն օգտագործում եմ չթեստավորված կրիտիկական կոդը գտնելու համար, ոչ թե մարդկանց գնահատելու, իսկ հիմնական մոդուլների վրա mutation testing-ով ստուգում եմ, որ թեստերն իրականում արդյունքներ են ստուգում։ Ողջամիտ գլոբալ շեմը, որն արգելափակում է մեծ անկումները, գումարած «սխալի յուրաքանչյուր ուղղում գալիս է թեստով» կանոնը, ավելի լավ հավաքածուներ են տալիս, քան որևէ մեկ տոկոս։
S4Թեստե՞ր, թե՞ մոնիթորինգ. ո՞ւր եք ներդնում, երբ ժամանակը սահմանափակ է։
Երկուսն էլ, բայց տարբեր ձախողումների դեմ։ Թեստերը բռնում են այն, ինչ կարող եք կանխատեսել, մինչև օգտատերերը կտեսնեն. տրամաբանական սխալներ, regression-ներ, կոտրված պայմանագրեր։ Մոնիթորինգը բռնում է այն, ինչ չէիք կարող կանխատեսել. տվյալների իրական ձևեր, երրորդ կողմի խափանումներ, արտադրողականություն իրական բեռի տակ, կոնֆիգուրացիայի սխալներ։ Սահմանափակ ժամանակի դեպքում ես feature թեստեր եմ գրում փողի, auth-ի և հիմնական վիճակների անցումների համար, որովհետև այդ ձախողումները թանկ են և կանխատեսելի։ Այնուհետև ավելացնում եմ սխալների հետևում ահազանգերով, health check-եր, կառուցվածքային լոգեր request id-ներով և մի քանի բիզնես մետրիկա, օրինակ՝ պատվերներ ժամում, որոնք հայտնաբերում են լուռ ձախողումներ, որոնք ոչ մի թեստ չէր կանխատեսել։ Feature flag-երը և արագ rollback-ները նվազեցնում են այն ամենի արժեքը, ինչ սպրդում է։ Փոխզիջումը բացահայտ է. թեստերը կանխարգելում են, observability-ն տալիս է արագ հայտնաբերում և վերականգնում, և պրոդուկտին անհրաժեշտ է երկուսի նվազագույնը։
Բառարան
- Test double
- Թեստում իրական կախվածության ցանկացած փոխարինող՝ dummy, stub, fake, spy կամ mock։
- Fake
- Աշխատող, բայց պարզեցված իրականացում, օրինակ՝ հիշողության մեջ վճարային gateway, որն օգտագործվում է իրականի փոխարեն։
- Stub
- Double, որը կանչերին պատրաստի պատասխաններ է վերադարձնում՝ չստուգելով, թե ինչպես է այն օգտագործվել։
- Mock
- Double, որը ստուգում է, որ իրեն կանչել են սպասված ձևով, օրինակ՝ մեկ անգամ՝ որոշակի արգումենտներով։
- Fixture
- Պատրաստված տվյալներ կամ ֆայլեր, որոնց վրա հենվում է թեստը, օրինակ՝ ձայնագրված API պատասխան կամ seed արված բազա։
- Flaky test
- Թեստ, որը երբեմն անցնում է, երբեմն ձախողվում՝ առանց կոդի որևէ փոփոխության։
- Property-based testing
- Ստուգում, որ կանոնը պահպանվում է բազմաթիվ գեներացված մուտքերի համար՝ մի քանի ձեռքով գրված օրինակների փոխարեն։
- Mutation score
- Դիտավորյալ ներմուծված սխալների այն մասը, որը թեստային հավաքածուն հայտնաբերում է. թեստերի ուժի չափանիշ։
- Contract test
- Թեստ, որը ստուգում է, որ մատակարարը և նրա սպառողները դեռ համաձայն են հարցման և պատասխանի ձևի շուրջ։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։