Exemple de provocări write-tests după șablon
Această pagină prezintă o provocare de tip write-tests completă pentru fiecare șablon care acceptă acest mod. Fiecare exemplu listează soluția de referință, testul de pornire cu care începe rezolvitorul, testele de referință ale autorului care validează provocarea și mutanții cu fișierul lor mutants.json.
Fiecare exemplu a fost rulat pe utilitarul de testare (test runner) real: testele de referință ale autorului trec pe soluția de referință și prind fiecare mutant, iar testul de pornire trece pe soluția de referință.
Ești nou la provocările write-tests?
Citește mai întâi provocările write-tests. Acesta explică categoriile de fișiere, cum funcționează mutanții și cum rulează validarea.
Exemplele reutilizează același exercițiu în cadrul șabloanelor similare, astfel încât să poți compara modul în care același comportament este testat în fiecare cadru de lucru (framework):
| Șabloane | Exercițiu |
|---|---|
| Node.js, Node.js cu TypeScript, Python | Funcții pure mici: sum, chunk, clamp |
| NestJS, Fastify, Hono | Un endpoint HTTP cu validare |
| Svelte, Vanilla JavaScript, Angular, React, Vue, SolidJS | Un contor cu o limită inferioară și un buton de resetare |
| Vanilla TypeScript, React cu TypeScript, Vue cu TypeScript, SolidJS cu TypeScript | O listă de sarcini (todo list) care ignoră elementele goale și duplicate |
Șabloane pentru terminal
Node.js
Utilitar de testare (test runner): Jest · Exemplu: Sum: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /sum.js |
| Fișiere de test de pornire | /sum.test.js (plus /main.js) |
| Teste de referință ale autorului | /sum.reference.test.js |
| Mutanți | negatives-bug, plus-one, type-coercion + mutants.json |
- Testele sunt scrise în CommonJS simplu: folosește
requirepentru a importa modulul soluției în același mod în care o facemain.js. - Numește fișierele de test
*.test.js. Acestea se află lângă soluția de referință, astfel încâtrequire('./sum')încarcă implementarea reală sau mutantul în timpul unei rulări cu mutanți. main.jsrămâne punctul de intrare pentru Rulare (Run), astfel încât rezolvitorii să poată apela funcția și să afișeze valori în consolă în timp ce o explorează.
Soluție de referință
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('sum expects two numbers');
}
return a + b;
}
module.exports = { sum };Test de pornire (de unde începe rezolvitorul)
const { sum } = require('./sum');
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
const { sum } = require('./sum');
describe('sum (reference suite)', () => {
it('adds two positive numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('adds negative numbers correctly', () => {
expect(sum(-2, -3)).toBe(-5);
expect(sum(-2, 5)).toBe(3);
});
it('throws a TypeError for non-number input', () => {
expect(() => sum('1', 2)).toThrow(TypeError);
expect(() => sum(1, undefined)).toThrow(TypeError);
});
});Mutantul negatives-bug înlocuiește /sum.js:
function sum(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('sum expects two numbers');
}
return Math.abs(a) + Math.abs(b);
}
module.exports = { sum };{
"negatives-bug": {
"label": "Negatives handling",
"description": "Negative numbers are summed incorrectly."
},
"plus-one": {
"label": "Off-by-one",
"description": "The result is off by one."
},
"type-coercion": {
"label": "Invalid input handling",
"description": "Non-number inputs are coerced instead of rejected."
}
}Node.js cu TypeScript
Utilitar de testare (test runner): Jest (ts-jest) · Exemplu: Chunk: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /chunk.ts |
| Fișiere de test de pornire | /chunk.test.ts (plus /main.ts) |
| Teste de referință ale autorului | /chunk.reference.test.ts |
| Mutanți | empty-input, invalid-size, off-by-one + mutants.json |
describe,itșiexpectsunt disponibile ca variabile globale. De asemenea, le poți importa din@jest/globalspentru o tipizare explicită.- Numește fișierele de test
*.test.ts. Erorile de tip (type errors) duc la eșecul fișierului de test, așa că menține mutanții corecți din punct de vedere al tipurilor: aceștia trebuie să se compileze folosind aceleași importuri ca și soluția de referință.
Soluție de referință
export function chunk<T>(list: T[], size: number): T[][] {
if (!Number.isInteger(size) || size < 1) throw new RangeError('size must be a positive integer');
const chunks: T[][] = [];
for (let index = 0; index < list.length; index += size) {
chunks.push(list.slice(index, index + size));
}
return chunks;
}Test de pornire (de unde începe rezolvitorul)
import { chunk } from './chunk';
describe('chunk', () => {
it('splits a list into evenly sized chunks', () => {
expect(chunk([1, 2, 3, 4], 2)).toEqual([
[1, 2],
[3, 4]
]);
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { chunk } from './chunk';
describe('chunk (reference suite)', () => {
it('keeps a shorter trailing chunk', () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});
it('throws a RangeError for non-positive sizes', () => {
expect(() => chunk([1], 0)).toThrow(RangeError);
expect(() => chunk([1], -2)).toThrow(RangeError);
});
it('returns an empty list for empty input', () => {
expect(chunk([], 3)).toEqual([]);
});
});Mutantul empty-input înlocuiește /chunk.ts:
export function chunk<T>(list: T[], size: number): T[][] {
if (!Number.isInteger(size) || size < 1) throw new RangeError('size must be a positive integer');
if (list.length === 0) return [[]];
const chunks: T[][] = [];
for (let index = 0; index < list.length; index += size) {
chunks.push(list.slice(index, index + size));
}
return chunks;
}{
"empty-input": {
"label": "Empty input",
"description": "Empty lists produce the wrong shape."
},
"invalid-size": {
"label": "Invalid size handling",
"description": "Bad sizes are swallowed instead of rejected."
},
"off-by-one": {
"label": "Off-by-one",
"description": "A boundary in the chunking loop is wrong."
}
}Python
Utilitar de testare (test runner): pytest · Exemplu: Clamp: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /clamp.py |
| Fișiere de test de pornire | /test_clamp.py (plus /main.py) |
| Teste de referință ale autorului | /test_clamp_reference.py |
| Mutanți | invalid-range, off-by-one, swapped-bounds + mutants.json |
- Testele importă soluția prin intermediul pachetului
challenge:from challenge.clamp import clamp. Intrarea pentru Rulare (main.py) importă fără prefix (from clamp import clamp). - Numește fișierele de test
test_*.py. Decoratorii@m.describe/@m.itdinpytest.markoferă arborelui de teste nume ușor de citit în panoul de rezultate. - Folosește
pytest.raisespentru a specifica comportamentul în caz de eroare, o sursă clasică de mutanți (o validare lipsă care nu mai aruncă o eroare).
Soluție de referință
def clamp(value, low, high):
"""Return value limited to the inclusive range [low, high]."""
if low > high:
raise ValueError('low must not be greater than high')
if value < low:
return low
if value > high:
return high
return valueTest de pornire (de unde începe rezolvitorul)
from pytest import mark as m
from challenge.clamp import clamp
@m.describe("clamp")
class TestClamp:
@m.it("Returns the value when it is inside the range")
def test_inside_range(self):
assert clamp(5, 0, 10) == 5
# Add tests until every hidden bug is caught.Teste de referință ale autorului
import pytest
from pytest import mark as m
from challenge.clamp import clamp
@m.describe("clamp (reference suite)")
class TestClampReference:
@m.it("Returns the value when it is inside the range")
def test_inside_range(self):
assert clamp(5, 0, 10) == 5
@m.it("Clamps values below the lower bound up to low")
def test_below_low(self):
assert clamp(-5, 0, 10) == 0
@m.it("Clamps values above the upper bound down to high")
def test_above_high(self):
assert clamp(15, 0, 10) == 10
@m.it("Raises ValueError when low is greater than high")
def test_invalid_range(self):
with pytest.raises(ValueError):
clamp(5, 10, 0)Mutantul invalid-range înlocuiește /clamp.py:
def clamp(value, low, high):
"""Return value limited to the inclusive range [low, high]."""
if value < low:
return low
if value > high:
return high
return value{
"invalid-range": {
"label": "Invalid range handling",
"description": "A lower bound above the upper bound is accepted instead of rejected."
},
"off-by-one": {
"label": "Off-by-one",
"description": "One of the boundaries is clamped to the wrong value."
},
"swapped-bounds": {
"label": "Swapped bounds",
"description": "Out-of-range values are clamped to the wrong end of the range."
}
}Șabloane de backend cu tester API
NestJS
Instrument de testare: Vitest + @nestjs/testing · Exemplu: Reducere: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /src/main.ts, /src/app.module.ts, /src/discount.controller.ts, /src/discount.service.ts |
| Fișiere de test de pornire | /discount.controller.test.ts |
| Teste de referință ale autorului | /discount.reference.test.ts |
| Mutanți | inverted-discount, no-rounding, percent-over-100 + mutants.json |
- Începe fiecare fișier de test cu
import 'reflect-metadata';și construiește un modul de testare cuTest.createTestingModule, înregistrând fiecare controller și furnizor de care are nevoie unitatea. - Directoarele mutanților oglindesc structura
src/:[Mutants]/percent-over-100/src/discount.service.tsînlocuieștesrc/discount.service.ts. - Păstrează fișierele de test în rădăcină și numește-le
*.test.ts. Previzualizarea web le exclude din procesul de build Nest, astfel încât testele rezolvitorului nu vor defecta niciodată serverul care rulează.
Soluție de referință
import { Module } from '@nestjs/common';
import { DiscountController } from './discount.controller';
import { DiscountService } from './discount.service';
@Module({
imports: [],
controllers: [DiscountController],
providers: [DiscountService]
})
export class AppModule {}import { Controller, Get, Query } from '@nestjs/common';
import { DiscountService } from './discount.service';
@Controller('discount')
export class DiscountController {
constructor(private readonly discountService: DiscountService) {}
@Get()
apply(@Query('price') price: string, @Query('percent') percent: string): { total: number } {
return { total: this.discountService.apply(Number(price), Number(percent)) };
}
}import { BadRequestException, Injectable } from '@nestjs/common';
@Injectable()
export class DiscountService {
/**
* Applies a percentage discount to a price and returns the discounted price,
* rounded to cents.
*/
apply(price: number, percent: number): number {
if (!Number.isFinite(price) || price < 0) {
throw new BadRequestException('price must be a non-negative number');
}
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
throw new BadRequestException('percent must be between 0 and 100');
}
const discounted = price * (1 - percent / 100);
return Math.round(discounted * 100) / 100;
}
}Test de pornire (de la ce pornește rezolvitorul)
import 'reflect-metadata';
import { describe, it, expect, beforeEach } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { DiscountController } from './src/discount.controller';
import { DiscountService } from './src/discount.service';
describe('DiscountController', () => {
let controller: DiscountController;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [DiscountController],
providers: [DiscountService]
}).compile();
controller = moduleRef.get<DiscountController>(DiscountController);
});
it('applies a 20% discount to a price of 100', () => {
expect(controller.apply('100', '20')).toEqual({ total: 80 });
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import 'reflect-metadata';
import { describe, it, expect, beforeEach } from 'vitest';
import { BadRequestException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { DiscountController } from './src/discount.controller';
import { DiscountService } from './src/discount.service';
describe('DiscountController (reference suite)', () => {
let controller: DiscountController;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [DiscountController],
providers: [DiscountService]
}).compile();
controller = moduleRef.get<DiscountController>(DiscountController);
});
it('applies a 20% discount to a price of 100', () => {
expect(controller.apply('100', '20')).toEqual({ total: 80 });
});
it('keeps the full price for a 0% discount', () => {
expect(controller.apply('100', '0')).toEqual({ total: 100 });
});
it('returns 0 for a 100% discount', () => {
expect(controller.apply('100', '100')).toEqual({ total: 0 });
});
it('rounds the discounted price to cents', () => {
expect(controller.apply('19.99', '15')).toEqual({ total: 16.99 });
});
it('rejects a percent above 100', () => {
expect(() => controller.apply('100', '150')).toThrow(BadRequestException);
});
it('rejects a negative percent', () => {
expect(() => controller.apply('100', '-5')).toThrow(BadRequestException);
});
});Mutantul inverted-discount înlocuiește /src/discount.service.ts:
import { BadRequestException, Injectable } from '@nestjs/common';
@Injectable()
export class DiscountService {
/**
* Applies a percentage discount to a price and returns the discounted price,
* rounded to cents.
*/
apply(price: number, percent: number): number {
if (!Number.isFinite(price) || price < 0) {
throw new BadRequestException('price must be a non-negative number');
}
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
throw new BadRequestException('percent must be between 0 and 100');
}
const discounted = price * (percent / 100);
return Math.round(discounted * 100) / 100;
}
}{
"inverted-discount": {
"label": "Inverted discount",
"description": "The discount amount is returned instead of the discounted price."
},
"no-rounding": {
"label": "No rounding",
"description": "The discounted price is not rounded to cents."
},
"percent-over-100": {
"label": "Percent over 100 accepted",
"description": "A percent above 100 is applied instead of rejected."
}
}Fastify
Instrument de testare: Vitest + app.inject() · Exemplu: Total coș: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /src/main.ts, /src/app.ts |
| Fișiere de test de pornire | /cart.test.ts |
| Teste de referință ale autorului | /cart.reference.test.ts |
| Mutanți | accepts-empty-cart, ignores-quantity, skips-first-item + mutants.json |
- Soluția de referință exportă
buildApp()dinsrc/app.ts, astfel încât testele construiesc aplicația fără a asocia un port și apeleazăapp.inject({ method, url, payload }). - Apelează întotdeauna
await app.close()înafterEach, altfel conexiunile deschise se scurg între fișierele de test. - Codurile de stare fac și ele parte din comportament: folosește aserțiuni pentru
statusCodeîn cazul cererilor respinse, nu doar pentru corpul răspunsului în scenariul de succes.
Soluția de referință
import Fastify, { type FastifyInstance } from 'fastify';
export interface CartItem {
price: number;
qty: number;
}
interface CartBody {
items?: CartItem[];
}
/**
* Sums price × qty over the items, rounded to cents.
*/
export function cartTotal(items: CartItem[]): number {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return Math.round(total * 100) / 100;
}
export function buildApp(): FastifyInstance {
const app = Fastify({ logger: false });
app.post<{ Body: CartBody }>('/cart/total', async (request, reply) => {
const items = request.body?.items;
if (!Array.isArray(items) || items.length === 0) {
return reply.code(400).send({ error: 'items must be a non-empty array' });
}
const isValidItem = (item: CartItem): boolean =>
Number.isFinite(item.price) && item.price >= 0 && Number.isInteger(item.qty) && item.qty >= 1;
if (!items.every(isValidItem)) {
return reply.code(400).send({ error: 'each item needs a non-negative price and a positive integer qty' });
}
return { total: cartTotal(items) };
});
return app;
}Testul de pornire (de la ce pornește rezolvitorul)
import { describe, it, expect, afterEach } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { buildApp } from './src/app';
describe('POST /cart/total', () => {
let app: FastifyInstance;
afterEach(async () => {
await app?.close();
});
it('sums two items', async () => {
app = buildApp();
const response = await app.inject({
method: 'POST',
url: '/cart/total',
payload: { items: [{ price: 2.5, qty: 2 }, { price: 1.25, qty: 4 }] }
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ total: 10 });
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { describe, it, expect, afterEach, beforeEach } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { buildApp, type CartItem } from './src/app';
describe('POST /cart/total (reference suite)', () => {
let app: FastifyInstance;
beforeEach(() => {
app = buildApp();
});
afterEach(async () => {
await app.close();
});
const total = (items: unknown): Promise<{ statusCode: number; body: unknown }> =>
app.inject({ method: 'POST', url: '/cart/total', payload: { items } }).then((response) => ({
statusCode: response.statusCode,
body: response.json()
}));
it('sums two items', async () => {
const items: CartItem[] = [
{ price: 2.5, qty: 2 },
{ price: 1.25, qty: 4 }
];
expect(await total(items)).toEqual({ statusCode: 200, body: { total: 10 } });
});
it('counts a single item', async () => {
expect(await total([{ price: 3, qty: 1 }])).toEqual({ statusCode: 200, body: { total: 3 } });
});
it('multiplies price by quantity', async () => {
expect(await total([{ price: 2, qty: 3 }])).toEqual({ statusCode: 200, body: { total: 6 } });
});
it('rounds the total to cents', async () => {
expect(await total([{ price: 0.1, qty: 3 }])).toEqual({ statusCode: 200, body: { total: 0.3 } });
});
it('rejects an empty cart with 400', async () => {
const response = await total([]);
expect(response.statusCode).toBe(400);
});
it('rejects a quantity below 1 with 400', async () => {
const response = await total([{ price: 2, qty: 0 }]);
expect(response.statusCode).toBe(400);
});
});Mutantul accepts-empty-cart înlocuiește /src/app.ts:
import Fastify, { type FastifyInstance } from 'fastify';
export interface CartItem {
price: number;
qty: number;
}
interface CartBody {
items?: CartItem[];
}
/**
* Sums price × qty over the items, rounded to cents.
*/
export function cartTotal(items: CartItem[]): number {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return Math.round(total * 100) / 100;
}
export function buildApp(): FastifyInstance {
const app = Fastify({ logger: false });
app.post<{ Body: CartBody }>('/cart/total', async (request, reply) => {
const items = request.body?.items;
if (!Array.isArray(items)) {
return reply.code(400).send({ error: 'items must be a non-empty array' });
}
const isValidItem = (item: CartItem): boolean =>
Number.isFinite(item.price) && item.price >= 0 && Number.isInteger(item.qty) && item.qty >= 1;
if (!items.every(isValidItem)) {
return reply.code(400).send({ error: 'each item needs a non-negative price and a positive integer qty' });
}
return { total: cartTotal(items) };
});
return app;
}{
"accepts-empty-cart": {
"label": "Empty cart accepted",
"description": "An empty cart answers with a total instead of being rejected."
},
"ignores-quantity": {
"label": "Quantity ignored",
"description": "Each item is counted once regardless of its quantity."
},
"skips-first-item": {
"label": "First item skipped",
"description": "The first item of the cart is left out of the total."
}
}Hono
Instrument de testare: Vitest + app.request() · Exemplu: Slugify: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluția de referință | /src/main.ts, /src/app.ts |
| Fișiere de test de pornire | /slug.test.ts |
| Teste de referință ale autorului | /slug.reference.test.ts |
| Mutanți | accepts-empty, edge-dashes, keeps-case + mutants.json |
- Soluția de referință exportă instanța
Honodinsrc/app.ts.app.request(path, init?)returnează un răspuns standardResponsefără a porni un server. - Citește corpul răspunsului cu
await response.json()sauawait response.text(), și verificăresponse.statuspentru cererile care trebuie respinse.
Soluția de referință
import { Hono } from 'hono';
export const app = new Hono();
/**
* Turns free text into a URL slug: trimmed, lowercased, non-alphanumeric runs
* collapsed into single dashes, no leading or trailing dash.
*/
export function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
app.get('/slug', (c) => {
const text = c.req.query('text');
if (!text || text.trim().length === 0) {
return c.json({ error: 'text query parameter is required' }, 400);
}
return c.json({ slug: slugify(text) });
});Testul de pornire (de la ce pornește rezolvitorul)
import { describe, it, expect } from 'vitest';
import { app } from './src/app';
describe('GET /slug', () => {
it('turns spaces into dashes', async () => {
const response = await app.request('/slug?text=hello%20world');
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ slug: 'hello-world' });
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { describe, it, expect } from 'vitest';
import { app } from './src/app';
describe('GET /slug (reference suite)', () => {
const slug = async (text: string): Promise<{ status: number; body: unknown }> => {
const response = await app.request(`/slug?text=${encodeURIComponent(text)}`);
return { status: response.status, body: await response.json() };
};
it('turns spaces into dashes', async () => {
expect(await slug('hello world')).toEqual({ status: 200, body: { slug: 'hello-world' } });
});
it('lowercases the text', async () => {
expect(await slug('DojoCode')).toEqual({ status: 200, body: { slug: 'dojocode' } });
});
it('collapses punctuation and strips leading/trailing dashes', async () => {
expect(await slug(' Hello, World! ')).toEqual({ status: 200, body: { slug: 'hello-world' } });
});
it('rejects a missing text parameter with 400', async () => {
const response = await app.request('/slug');
expect(response.status).toBe(400);
});
it('rejects an empty text parameter with 400', async () => {
expect((await slug('')).status).toBe(400);
expect((await slug(' ')).status).toBe(400);
});
});Mutantul accepts-empty înlocuiește /src/app.ts:
import { Hono } from 'hono';
export const app = new Hono();
/**
* Turns free text into a URL slug: trimmed, lowercased, non-alphanumeric runs
* collapsed into single dashes, no leading or trailing dash.
*/
export function slugify(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
app.get('/slug', (c) => {
const text = c.req.query('text');
if (text === undefined) {
return c.json({ error: 'text query parameter is required' }, 400);
}
return c.json({ slug: slugify(text) });
});{
"accepts-empty": {
"label": "Empty text accepted",
"description": "An empty text parameter answers with an empty slug instead of being rejected."
},
"edge-dashes": {
"label": "Edge dashes kept",
"description": "Leading and trailing dashes are not stripped from the slug."
},
"keeps-case": {
"label": "Case kept",
"description": "The slug keeps the original casing instead of being lowercased."
}
}Șabloane pentru browser
Svelte
Rulor de teste: Jest + @testing-library/svelte · Exemplu: Svelte Counter: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /App.svelte |
| Fișiere de test de pornire | /App.spec.js (plus /index.js) |
| Teste de referință ale autorului | /App.reference.spec.js |
| Mutanți | double-step, negative-allowed, reset-guard + mutants.json |
- Matcherii
@testing-library/jest-dom(toBeInTheDocument,toBeDisabled) sunt înregistrați pentru tine. - Folosește
awaitpentru fiecare apelfireEvent: Svelte aplică actualizările DOM în mod asincron, iar o verificare făcută imediat după un clic fărăawaitva citi starea veche. - Numește fișierele de test
*.spec.jsși apeleazăcleanup()înafterEach.
Soluția de referință
<script>
let count = 0;
function increment() {
count += 1;
}
function decrement() {
if (count > 0) count -= 1;
}
function reset() {
count = 0;
}
</script>
<main>
<p>Count: {count}</p>
<button on:click={decrement} disabled={count === 0}>Decrement</button>
<button on:click={increment}>Increment</button>
<button on:click={reset}>Reset</button>
</main>Test de pornire (de unde începe cel care rezolvă)
import { render, screen, fireEvent, cleanup } from "@testing-library/svelte";
import App from "./App.svelte";
afterEach(() => cleanup());
describe("Counter", () => {
test("starts at zero", () => {
render(App);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { render, screen, fireEvent, cleanup } from "@testing-library/svelte";
import App from "./App.svelte";
afterEach(() => cleanup());
const button = (name) => screen.getByRole("button", { name });
describe("Counter (reference suite)", () => {
test("starts at zero", () => {
render(App);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
test("increments by one", async () => {
render(App);
await fireEvent.click(button("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
test("decrements by one", async () => {
render(App);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
test("never goes below zero", async () => {
render(App);
expect(button("Decrement")).toBeDisabled();
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await fireEvent.click(button("Increment"));
expect(button("Decrement")).not.toBeDisabled();
});
test("resets to zero after a single increment", async () => {
render(App);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Reset"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
});Mutantul double-step înlocuiește /App.svelte:
<script>
let count = 0;
function increment() {
count += 2;
}
function decrement() {
if (count > 0) count -= 1;
}
function reset() {
count = 0;
}
</script>
<main>
<p>Count: {count}</p>
<button on:click={decrement} disabled={count === 0}>Decrement</button>
<button on:click={increment}>Increment</button>
<button on:click={reset}>Reset</button>
</main>{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}Vanilla JavaScript
Rulor de teste: Jest + jsdom · Exemplu: Vanilla JS Counter: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /counter.js |
| Fișiere de test de pornire | /counter.spec.js (plus /index.js, /index.html) |
| Teste de referință ale autorului | /counter.reference.spec.js |
| Mutanți | double-step, negative-allowed, reset-guard + mutants.json |
- Nu există Testing Library în această imagine: interoghează DOM-ul cu
document.querySelectorși declanșează evenimente cu.click(). - Configurarea testului expune fișierul
index.htmlcaglobalThis.htmlContent. Resetează documentul înbeforeEach(document.documentElement.innerHTML = globalThis.htmlContent) pentru ca fiecare test să înceapă de la zero, apoi montează în#app. - Numește fișierele de test
*.spec.js.
Soluția de referință
export function createCounter(root) {
let count = 0;
const display = document.createElement("p");
const decrementButton = document.createElement("button");
const incrementButton = document.createElement("button");
const resetButton = document.createElement("button");
decrementButton.textContent = "Decrement";
incrementButton.textContent = "Increment";
resetButton.textContent = "Reset";
function render() {
display.textContent = `Count: ${count}`;
decrementButton.disabled = count === 0;
}
incrementButton.addEventListener("click", () => {
count += 1;
render();
});
decrementButton.addEventListener("click", () => {
if (count > 0) count -= 1;
render();
});
resetButton.addEventListener("click", () => {
count = 0;
render();
});
root.append(display, decrementButton, incrementButton, resetButton);
render();
}Test inițial (de la ce pornește rezolvitorul)
import { createCounter } from './counter';
describe('counter.js', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createCounter(document.getElementById('app'));
});
test('starts at zero', () => {
expect(document.querySelector('p').textContent).toEqual('Count: 0');
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { createCounter } from './counter';
const button = (label) => [...document.querySelectorAll('button')].find((node) => node.textContent === label);
const count = () => document.querySelector('p').textContent;
describe('counter.js (reference suite)', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createCounter(document.getElementById('app'));
});
test('starts at zero', () => {
expect(count()).toEqual('Count: 0');
});
test('increments by one', () => {
button('Increment').click();
expect(count()).toEqual('Count: 1');
});
test('decrements by one', () => {
button('Increment').click();
button('Increment').click();
button('Decrement').click();
expect(count()).toEqual('Count: 1');
});
test('never goes below zero', () => {
expect(button('Decrement').disabled).toBe(true);
button('Decrement').click();
expect(count()).toEqual('Count: 0');
button('Increment').click();
expect(button('Decrement').disabled).toBe(false);
});
test('resets to zero after a single increment', () => {
button('Increment').click();
button('Reset').click();
expect(count()).toEqual('Count: 0');
});
});Mutantul `double-step` înlocuiește `/counter.js`:
export function createCounter(root) {
let count = 0;
const display = document.createElement("p");
const decrementButton = document.createElement("button");
const incrementButton = document.createElement("button");
const resetButton = document.createElement("button");
decrementButton.textContent = "Decrement";
incrementButton.textContent = "Increment";
resetButton.textContent = "Reset";
function render() {
display.textContent = `Count: ${count}`;
decrementButton.disabled = count === 0;
}
incrementButton.addEventListener("click", () => {
count += 2;
render();
});
decrementButton.addEventListener("click", () => {
if (count > 0) count -= 1;
render();
});
resetButton.addEventListener("click", () => {
count = 0;
render();
});
root.append(display, decrementButton, incrementButton, resetButton);
render();
}{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}Vanilla TypeScript
Runner de teste: Jest (ts-jest) + jsdom · Exemplu: Vanilla TS Todo List: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /todoList.ts |
| Fișiere de test inițiale | /todoList.spec.ts (plus /index.ts, /index.html) |
| Teste de referință ale autorului | /todoList.reference.spec.ts |
| Mutanți | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Aceeași configurare DOM ca la Vanilla JavaScript: resetează documentul din
globalThis.htmlContentînbeforeEachși montează-l în#app. - Biblioteca (
lib) de TypeScript nu includeDOM.Iterable, așa că descompunerea unuiNodeList([...document.querySelectorAll('li')]) nu se compilează. Folosește în schimbArray.from(...). - Importă globalii Jest din
@jest/globalsși numește fișierele de test*.spec.ts.
Soluția de referință
export function createTodoList(root: HTMLElement): void {
const items: string[] = [];
const heading = document.createElement("h1");
const input = document.createElement("input");
const addButton = document.createElement("button");
const list = document.createElement("ul");
input.placeholder = "What needs to be done?";
addButton.textContent = "Add";
function render(): void {
heading.textContent = `Todos (${items.length})`;
list.replaceChildren(
...items.map((item) => {
const li = document.createElement("li");
li.textContent = item;
return li;
})
);
}
addButton.addEventListener("click", () => {
const text = input.value.trim();
if (text.length === 0) return;
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.push(text);
input.value = "";
render();
});
root.append(heading, input, addButton, list);
render();
}Test inițial (de la ce pornește rezolvitorul)
import { createTodoList } from './todoList';
import { describe, test, expect, beforeEach } from '@jest/globals';
describe('todoList.ts', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createTodoList(document.getElementById('app') as HTMLElement);
});
test('starts empty', () => {
expect(document.querySelector('h1').textContent).toEqual('Todos (0)');
expect(document.querySelectorAll('li')).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { createTodoList } from './todoList';
import { describe, test, expect, beforeEach } from '@jest/globals';
const input = (): HTMLInputElement => document.querySelector('input');
const heading = (): string => document.querySelector('h1').textContent;
const items = (): string[] => Array.from(document.querySelectorAll('li')).map((node) => node.textContent);
function add(text: string): void {
input().value = text;
document.querySelector('button').click();
}
describe('todoList.ts (reference suite)', () => {
beforeEach(() => {
document.documentElement.innerHTML = globalThis.htmlContent;
createTodoList(document.getElementById('app') as HTMLElement);
});
test('starts empty', () => {
expect(heading()).toEqual('Todos (0)');
expect(items()).toEqual([]);
});
test('adds a trimmed todo and counts it', () => {
add(' Buy milk ');
expect(items()).toEqual(['Buy milk']);
expect(heading()).toEqual('Todos (1)');
});
test('clears the input after adding', () => {
add('Buy milk');
expect(input().value).toEqual('');
});
test('ignores blank todos', () => {
add('');
add(' ');
expect(items()).toEqual([]);
expect(heading()).toEqual('Todos (0)');
});
test('ignores duplicates regardless of letter case', () => {
add('Buy milk');
add('buy MILK');
expect(items()).toEqual(['Buy milk']);
expect(heading()).toEqual('Todos (1)');
});
});Mutantul `accepts-blank` înlocuiește `/todoList.ts`:
export function createTodoList(root: HTMLElement): void {
const items: string[] = [];
const heading = document.createElement("h1");
const input = document.createElement("input");
const addButton = document.createElement("button");
const list = document.createElement("ul");
input.placeholder = "What needs to be done?";
addButton.textContent = "Add";
function render(): void {
heading.textContent = `Todos (${items.length})`;
list.replaceChildren(
...items.map((item) => {
const li = document.createElement("li");
li.textContent = item;
return li;
})
);
}
addButton.addEventListener("click", () => {
const text = input.value.trim();
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.push(text);
input.value = "";
render();
});
root.append(heading, input, addButton, list);
render();
}{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}Angular
Test runner: Jest (jest-preset-angular) + @testing-library/angular · Exemplu: Angular Counter: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /app.component.ts, /app.component.html |
| Fișiere de test de pornire | /app.component.spec.ts (plus /angular.json, /app.module.ts, /index.html, /index.ts, /style.css) |
| Teste de referință ale autorului | /app.component.reference.spec.ts |
| Mutanți | double-step, negative-allowed, reset-guard + mutants.json |
- Această imagine nu are matcheri
jest-dom. Fă aserțiuni direct cu Jest simplu în schimb:expect(screen.getByText('Count: 0')).toBeTruthy()șiexpect(button.disabled).toBe(true). - Șablonul componentei (
app.component.html) face parte din soluția de referință. Un mutant poate înlocui fie fișierul.ts, fie fișierul.html. - Numește fișierele de test
*.spec.ts. Build-ul Angular care rulează previzualizarea web exclude*.spec.ts, așa că orice alt nume ar strica previzualizarea.
Soluția de referință
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: []
})
export class AppComponent {
count = 0;
get canDecrement(): boolean {
return this.count > 0;
}
increment(): void {
this.count += 1;
}
decrement(): void {
if (this.count > 0) this.count -= 1;
}
reset(): void {
this.count = 0;
}
}<div>
<p>Count: {{ count }}</p>
<button (click)="decrement()" [disabled]="!canDecrement">Decrement</button>
<button (click)="increment()">Increment</button>
<button (click)="reset()">Reset</button>
</div>Testul de pornire (de la care pornește rezolvitorul)
import { render, screen, fireEvent } from '@testing-library/angular';
import { AppComponent } from './app.component';
describe('Counter', () => {
it('starts at zero', async () => {
await render(AppComponent);
expect(screen.getByText('Count: 0')).toBeTruthy();
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { render, screen, fireEvent } from '@testing-library/angular';
import { AppComponent } from './app.component';
const button = (name: string): HTMLButtonElement => screen.getByRole('button', { name }) as HTMLButtonElement;
describe('Counter (reference suite)', () => {
it('starts at zero', async () => {
await render(AppComponent);
expect(screen.getByText('Count: 0')).toBeTruthy();
});
it('increments by one', async () => {
await render(AppComponent);
fireEvent.click(button('Increment'));
expect(screen.getByText('Count: 1')).toBeTruthy();
});
it('decrements by one', async () => {
await render(AppComponent);
fireEvent.click(button('Increment'));
fireEvent.click(button('Increment'));
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 1')).toBeTruthy();
});
it('never goes below zero', async () => {
await render(AppComponent);
expect(button('Decrement').disabled).toBe(true);
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 0')).toBeTruthy();
fireEvent.click(button('Increment'));
expect(button('Decrement').disabled).toBe(false);
});
it('resets to zero after a single increment', async () => {
await render(AppComponent);
fireEvent.click(button('Increment'));
fireEvent.click(button('Reset'));
expect(screen.getByText('Count: 0')).toBeTruthy();
});
});Mutant double-step înlocuiește /app.component.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: []
})
export class AppComponent {
count = 0;
get canDecrement(): boolean {
return this.count > 0;
}
increment(): void {
this.count += 2;
}
decrement(): void {
if (this.count > 0) this.count -= 1;
}
reset(): void {
this.count = 0;
}
}{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}React
Test runner: Jest + @testing-library/react · Exemplu: React Counter: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /App.jsx |
| Fișiere de test de pornire | /App.spec.js (plus /index.jsx) |
| Teste de referință ale autorului | /App.reference.spec.js |
| Mutanți | double-step, negative-allowed, reset-guard + mutants.json |
- Folosește JSX în fișierele
*.spec.js: imaginea le compilează cu preset-ul React al Babel. - Caută după rol (
getByRole('button', { name: 'Increment' })) pentru ca testele să urmărească ceea ce văd utilizatorii, apoi foloseștefireEvent.clickși fă aserțiuni pe noul text. - Apelează
cleanupînafterEachpentru ca componentele din testele anterioare să nu rămână montate.
Soluția de referință
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const increment = () => setCount((current) => current + 1);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={decrement} disabled={count === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default App;Testul de pornire (de la care pornește rezolvitorul)
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import App from './App.jsx';
afterEach(cleanup);
describe('Counter', () => {
test('starts at zero', () => {
render(<App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Testele de referință ale autorului
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import App from './App.jsx';
afterEach(cleanup);
const button = (name) => screen.getByRole('button', { name });
describe('Counter (reference suite)', () => {
test('starts at zero', () => {
render(<App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('increments by one', () => {
render(<App />);
fireEvent.click(button('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements by one', () => {
render(<App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Increment'));
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('never goes below zero', () => {
render(<App />);
expect(button('Decrement')).toBeDisabled();
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(button('Increment'));
expect(button('Decrement')).not.toBeDisabled();
});
test('resets to zero after a single increment', () => {
render(<App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Reset'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
});Mutantul double-step înlocuiește /App.jsx:
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const increment = () => setCount((current) => current + 2);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={decrement} disabled={count === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default App;{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}React cu TypeScript
Runner de teste: Jest (ts-jest) + @testing-library/react · Exemplu: React TS Todo List: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluția de referință | /App.tsx |
| Fișiere de test inițiale | /App.spec.ts (plus /index.tsx) |
| Testele de referință ale autorului | /App.reference.spec.ts |
| Mutanți | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Fișierele de test sunt
*.spec.ts, nu.tsx: randează cuReact.createElement(App)în loc de JSX. - Importă mai întâi
@testing-library/jest-dom/jest-globalspentru catoBeInTheDocument()să fie tipizat și înregistrat. - Pentru inputuri controlate, folosește
fireEvent.change(input, { target: { value } }).
Soluția de referință
import React, { useState } from "react";
const App: React.FC = () => {
const [items, setItems] = useState<string[]>([]);
const [draft, setDraft] = useState("");
const addTodo = (): void => {
const text = draft.trim();
if (text.length === 0) return;
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items, text]);
setDraft("");
};
return (
<div>
<h1>Todos ({items.length})</h1>
<input
placeholder="What needs to be done?"
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export default App;Test inițial (de la ce pornește rezolvitorul)
import '@testing-library/jest-dom/jest-globals';
import * as React from "react";
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import App from "./App";
describe("TodoList", () => {
it("starts empty", () => {
render(React.createElement(App));
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Testele de referință ale autorului
import '@testing-library/jest-dom/jest-globals';
import * as React from "react";
import { describe, expect, it } from '@jest/globals';
import { fireEvent, render, screen } from '@testing-library/react';
import App from "./App";
const input = (): HTMLInputElement => screen.getByPlaceholderText("What needs to be done?") as HTMLInputElement;
const items = (): string[] => screen.queryAllByRole("listitem").map((node) => node.textContent ?? "");
function add(text: string): void {
fireEvent.change(input(), { target: { value: text } });
fireEvent.click(screen.getByRole("button", { name: "Add" }));
}
describe("TodoList (reference suite)", () => {
it("starts empty", () => {
render(React.createElement(App));
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(items()).toEqual([]);
});
it("adds a trimmed todo and counts it", () => {
render(React.createElement(App));
add(" Buy milk ");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
it("clears the input after adding", () => {
render(React.createElement(App));
add("Buy milk");
expect(input().value).toBe("");
});
it("ignores blank todos", () => {
render(React.createElement(App));
add("");
add(" ");
expect(items()).toEqual([]);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
});
it("ignores duplicates regardless of letter case", () => {
render(React.createElement(App));
add("Buy milk");
add("buy MILK");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
});Mutantul accepts-blank înlocuiește /App.tsx:
import React, { useState } from "react";
const App: React.FC = () => {
const [items, setItems] = useState<string[]>([]);
const [draft, setDraft] = useState("");
const addTodo = (): void => {
const text = draft.trim();
if (items.some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items, text]);
setDraft("");
};
return (
<div>
<h1>Todos ({items.length})</h1>
<input
placeholder="What needs to be done?"
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export default App;{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}Vue
Runner de teste: Jest + @testing-library/vue · Exemplu: Vue Counter: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluția de referință | /App.vue |
| Fișiere de test inițiale | /App.spec.js (plus /index.js) |
| Testele de referință ale autorului | /App.reference.spec.js |
| Mutanți | double-step, negative-allowed, reset-guard + mutants.json |
- Importă
@testing-library/jest-dom/jest-globalsși preiadescribe,it,expectdin@jest/globals. - Folosește
awaitpentru fiecare apelfireEvent: Vue actualizează DOM-ul la următorul tick. - Numește fișierele de test
*.spec.js.
Soluția de referință
<template>
<div>
<p>Count: {{ count }}</p>
<button :disabled="count === 0" @click="decrement">Decrement</button>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value += 1;
}
function decrement() {
if (count.value > 0) count.value -= 1;
}
function reset() {
count.value = 0;
}
</script>Testul inițial (de la ce pornește rezolvitorul)
import '@testing-library/jest-dom/jest-globals';
import { render, fireEvent } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import Counter from "./App.vue";
describe('Counter', () => {
it("starts at zero", () => {
render(Counter);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Testele de referință ale autorului
import '@testing-library/jest-dom/jest-globals';
import { render, fireEvent } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import Counter from "./App.vue";
const button = (name) => screen.getByRole("button", { name });
describe('Counter (reference suite)', () => {
it("starts at zero", () => {
render(Counter);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
it("increments by one", async () => {
render(Counter);
await fireEvent.click(button("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
it("decrements by one", async () => {
render(Counter);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
it("never goes below zero", async () => {
render(Counter);
expect(button("Decrement")).toBeDisabled();
await fireEvent.click(button("Decrement"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await fireEvent.click(button("Increment"));
expect(button("Decrement")).not.toBeDisabled();
});
it("resets to zero after a single increment", async () => {
render(Counter);
await fireEvent.click(button("Increment"));
await fireEvent.click(button("Reset"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
});Mutantul double-step înlocuiește /App.vue:
<template>
<div>
<p>Count: {{ count }}</p>
<button :disabled="count === 0" @click="decrement">Decrement</button>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value += 2;
}
function decrement() {
if (count.value > 0) count.value -= 1;
}
function reset() {
count.value = 0;
}
</script>{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}Vue cu TypeScript
Test runner: Jest (ts-jest) + @testing-library/vue · Exemplu: Vue TS Todo List: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluția de referință | /App.vue |
| Fișiere de test inițiale | /App.spec.ts (plus /index.ts) |
| Teste de referință ale autorului | /App.reference.spec.ts |
| Mutanți | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Aceeași configurare ca la Vue, cu fișiere de test
*.spec.tsși componente<script lang="ts" setup>. - Pentru inputurile
v-model, foloseșteawait fireEvent.update(input, value): setează valoarea și declanșează evenimentul corect într-un singur apel.
Soluția de referință
<template>
<div>
<h1>Todos ({{ items.length }})</h1>
<input v-model="draft" placeholder="What needs to be done?" />
<button @click="addTodo">Add</button>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const items = ref<string[]>([]);
const draft = ref("");
function addTodo(): void {
const text = draft.value.trim();
if (text.length === 0) return;
if (items.value.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.value.push(text);
draft.value = "";
}
</script>Testul inițial (de la ce pornește rezolvitorul)
import '@testing-library/jest-dom/jest-globals';
import { render } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import TodoList from "./App.vue";
describe('TodoList', () => {
it("starts empty", () => {
render(TodoList);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Testele de referință ale autorului
import '@testing-library/jest-dom/jest-globals';
import { render, fireEvent } from "@testing-library/vue";
import { describe, expect, it } from '@jest/globals';
import { screen } from "@testing-library/dom";
import TodoList from "./App.vue";
const input = (): HTMLInputElement => screen.getByPlaceholderText("What needs to be done?") as HTMLInputElement;
const items = (): string[] => screen.queryAllByRole("listitem").map((node) => node.textContent ?? "");
async function add(text: string): Promise<void> {
await fireEvent.update(input(), text);
await fireEvent.click(screen.getByRole("button", { name: "Add" }));
}
describe('TodoList (reference suite)', () => {
it("starts empty", () => {
render(TodoList);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
expect(items()).toEqual([]);
});
it("adds a trimmed todo and counts it", async () => {
render(TodoList);
await add(" Buy milk ");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
it("clears the input after adding", async () => {
render(TodoList);
await add("Buy milk");
expect(input().value).toBe("");
});
it("ignores blank todos", async () => {
render(TodoList);
await add("");
await add(" ");
expect(items()).toEqual([]);
expect(screen.getByText("Todos (0)")).toBeInTheDocument();
});
it("ignores duplicates regardless of letter case", async () => {
render(TodoList);
await add("Buy milk");
await add("buy MILK");
expect(items()).toEqual(["Buy milk"]);
expect(screen.getByText("Todos (1)")).toBeInTheDocument();
});
});Mutantul accepts-blank înlocuiește /App.vue:
<template>
<div>
<h1>Todos ({{ items.length }})</h1>
<input v-model="draft" placeholder="What needs to be done?" />
<button @click="addTodo">Add</button>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const items = ref<string[]>([]);
const draft = ref("");
function addTodo(): void {
const text = draft.value.trim();
if (items.value.some((item) => item.toLowerCase() === text.toLowerCase())) return;
items.value.push(text);
draft.value = "";
}
</script>{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}SolidJS
Test runner: Vitest + @solidjs/testing-library · Exemplu: Solid Counter: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /App.jsx |
| Fișiere de test inițiale | /App.spec.jsx (plus /main.jsx) |
| Teste de referință ale autorului | /App.reference.spec.jsx |
| Mutanți | double-step, negative-allowed, reset-guard + mutants.json |
- Randează cu o funcție:
render(() => <App />). Matcher-iijest-domsunt înregistrați de configurarea testului. - Solid actualizează DOM-ul în mod sincron, astfel încât aserțiunile pot urma direct după
fireEvent.click. - Numește fișierele de test
*.spec.jsx.
Soluția de referință
import { createSignal } from 'solid-js';
export default function App() {
const [count, setCount] = createSignal(0);
const increment = () => setCount((current) => current + 1);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={decrement} disabled={count() === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}Test inițial (de unde pornește cel care rezolvă)
import { render, screen, fireEvent } from '@solidjs/testing-library';
import App from './App';
describe('Counter', () => {
test('starts at zero', () => {
render(() => <App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { render, screen, fireEvent } from '@solidjs/testing-library';
import App from './App';
const button = (name) => screen.getByRole('button', { name });
describe('Counter (reference suite)', () => {
test('starts at zero', () => {
render(() => <App />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('increments by one', () => {
render(() => <App />);
fireEvent.click(button('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements by one', () => {
render(() => <App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Increment'));
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('never goes below zero', () => {
render(() => <App />);
expect(button('Decrement')).toBeDisabled();
fireEvent.click(button('Decrement'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(button('Increment'));
expect(button('Decrement')).not.toBeDisabled();
});
test('resets to zero after a single increment', () => {
render(() => <App />);
fireEvent.click(button('Increment'));
fireEvent.click(button('Reset'));
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
});Mutantul double-step înlocuiește /App.jsx:
import { createSignal } from 'solid-js';
export default function App() {
const [count, setCount] = createSignal(0);
const increment = () => setCount((current) => current + 2);
const decrement = () => setCount((current) => (current > 0 ? current - 1 : current));
const reset = () => setCount(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={decrement} disabled={count() === 0}>
Decrement
</button>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}{
"double-step": {
"label": "Double step",
"description": "Increment adds 2 instead of 1."
},
"negative-allowed": {
"label": "Negative count allowed",
"description": "Decrement keeps working below zero."
},
"reset-guard": {
"label": "Reset guard",
"description": "Reset does nothing when the count is 1."
}
}SolidJS cu TypeScript
Test runner: Vitest + @solidjs/testing-library · Exemplu: Solid TS Todo List: Scrie testele
| Categorie | Fișiere |
|---|---|
| Soluție de referință | /App.tsx |
| Fișiere de test inițiale | /App.spec.tsx (plus /main.tsx) |
| Teste de referință ale autorului | /App.reference.spec.tsx |
| Mutanți | accepts-blank, case-sensitive-duplicates, keeps-input + mutants.json |
- Aceeași configurare ca și pentru SolidJS, cu fișiere de test
*.spec.tsx. - Pentru inputuri legate cu
onInput, foloseștefireEvent.input(input, { target: { value } }).
Soluția de referință
import { createSignal, For } from 'solid-js';
export default function App() {
const [items, setItems] = createSignal<string[]>([]);
const [draft, setDraft] = createSignal('');
const addTodo = (): void => {
const text = draft().trim();
if (text.length === 0) return;
if (items().some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items(), text]);
setDraft('');
};
return (
<div>
<h1>Todos ({items().length})</h1>
<input
placeholder="What needs to be done?"
value={draft()}
onInput={(event) => setDraft(event.currentTarget.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
<For each={items()}>{(item) => <li>{item}</li>}</For>
</ul>
</div>
);
}Test inițial (de unde pornește cel care rezolvă)
import { render, screen } from '@solidjs/testing-library';
import App from './App';
describe('TodoList', () => {
test('starts empty', () => {
render(() => <App />);
expect(screen.getByText('Todos (0)')).toBeInTheDocument();
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
});
// Add tests until every hidden bug is caught.
});Teste de referință ale autorului
import { render, screen, fireEvent } from '@solidjs/testing-library';
import App from './App';
const input = (): HTMLInputElement => screen.getByPlaceholderText('What needs to be done?') as HTMLInputElement;
const items = (): string[] => screen.queryAllByRole('listitem').map((node) => node.textContent ?? '');
function add(text: string): void {
fireEvent.input(input(), { target: { value: text } });
fireEvent.click(screen.getByRole('button', { name: 'Add' }));
}
describe('TodoList (reference suite)', () => {
test('starts empty', () => {
render(() => <App />);
expect(screen.getByText('Todos (0)')).toBeInTheDocument();
expect(items()).toEqual([]);
});
test('adds a trimmed todo and counts it', () => {
render(() => <App />);
add(' Buy milk ');
expect(items()).toEqual(['Buy milk']);
expect(screen.getByText('Todos (1)')).toBeInTheDocument();
});
test('clears the input after adding', () => {
render(() => <App />);
add('Buy milk');
expect(input().value).toBe('');
});
test('ignores blank todos', () => {
render(() => <App />);
add('');
add(' ');
expect(items()).toEqual([]);
expect(screen.getByText('Todos (0)')).toBeInTheDocument();
});
test('ignores duplicates regardless of letter case', () => {
render(() => <App />);
add('Buy milk');
add('buy MILK');
expect(items()).toEqual(['Buy milk']);
expect(screen.getByText('Todos (1)')).toBeInTheDocument();
});
});Mutantul accepts-blank înlocuiește /App.tsx:
import { createSignal, For } from 'solid-js';
export default function App() {
const [items, setItems] = createSignal<string[]>([]);
const [draft, setDraft] = createSignal('');
const addTodo = (): void => {
const text = draft().trim();
if (items().some((item) => item.toLowerCase() === text.toLowerCase())) return;
setItems([...items(), text]);
setDraft('');
};
return (
<div>
<h1>Todos ({items().length})</h1>
<input
placeholder="What needs to be done?"
value={draft()}
onInput={(event) => setDraft(event.currentTarget.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
<For each={items()}>{(item) => <li>{item}</li>}</For>
</ul>
</div>
);
}{
"accepts-blank": {
"label": "Blank todos accepted",
"description": "An empty or whitespace-only todo is added to the list."
},
"case-sensitive-duplicates": {
"label": "Case-sensitive duplicates",
"description": "A todo that differs from an existing one only by letter case is added again."
},
"keeps-input": {
"label": "Input not cleared",
"description": "The input keeps its text after a todo is added."
}
}Pagini similare
- Provocări de scriere de teste: ghid de redactare, categorii de fișiere și validare
- Rezolvarea unei provocări de scriere de teste: ce vede rezolvitorul și cum sunt evaluate rezultatele
- Creează o provocare de cod