Skip to content

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):

ȘabloaneExercițiu
Node.js, Node.js cu TypeScript, PythonFuncții pure mici: sum, chunk, clamp
NestJS, Fastify, HonoUn endpoint HTTP cu validare
Svelte, Vanilla JavaScript, Angular, React, Vue, SolidJSUn contor cu o limită inferioară și un buton de resetare
Vanilla TypeScript, React cu TypeScript, Vue cu TypeScript, SolidJS cu TypeScriptO 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

CategorieFiș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ținegatives-bug, plus-one, type-coercion + mutants.json
  • Testele sunt scrise în CommonJS simplu: folosește require pentru a importa modulul soluției în același mod în care o face main.js.
  • Numește fișierele de test *.test.js. Acestea se află lângă soluția de referință, astfel încât require('./sum') încarcă implementarea reală sau mutantul în timpul unei rulări cu mutanți.
  • main.js ră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ță

js
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)

js
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
js
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:

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 };
json
{
  "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

CategorieFiș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țiempty-input, invalid-size, off-by-one + mutants.json
  • describe, it și expect sunt disponibile ca variabile globale. De asemenea, le poți importa din @jest/globals pentru 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ță

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');

  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)

ts
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
ts
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:

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;
}
json
{
  "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

CategorieFiș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țiinvalid-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.it din pytest.mark oferă arborelui de teste nume ușor de citit în panoul de rezultate.
  • Folosește pytest.raises pentru 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ță

python
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 value

Test de pornire (de unde începe rezolvitorul)

python
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
python
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:

python
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
json
{
  "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

CategorieFiș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țiinverted-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 cu Test.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ște src/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ță

ts
import { Module } from '@nestjs/common';
import { DiscountController } from './discount.controller';
import { DiscountService } from './discount.service';

@Module({
  imports: [],
  controllers: [DiscountController],
  providers: [DiscountService]
})
export class AppModule {}
ts
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)) };
  }
}
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 * (1 - percent / 100);

    return Math.round(discounted * 100) / 100;
  }
}

Test de pornire (de la ce pornește rezolvitorul)

ts
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
ts
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:

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;
  }
}
json
{
  "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

CategorieFiș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țiaccepts-empty-cart, ignores-quantity, skips-first-item + mutants.json
  • Soluția de referință exportă buildApp() din src/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() în afterEach, 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ță

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) || 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)

ts
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
ts
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:

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;
}
json
{
  "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

CategorieFiș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țiaccepts-empty, edge-dashes, keeps-case + mutants.json
  • Soluția de referință exportă instanța Hono din src/app.ts. app.request(path, init?) returnează un răspuns standard Response fără a porni un server.
  • Citește corpul răspunsului cu await response.json() sau await response.text(), și verifică response.status pentru cererile care trebuie respinse.

Soluția de referință

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 || 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)

ts
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
ts
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:

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) });
});
json
{
  "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

CategorieFiș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țidouble-step, negative-allowed, reset-guard + mutants.json
  • Matcherii @testing-library/jest-dom (toBeInTheDocument, toBeDisabled) sunt înregistrați pentru tine.
  • Folosește await pentru fiecare apel fireEvent: Svelte aplică actualizările DOM în mod asincron, iar o verificare făcută imediat după un clic fără await va citi starea veche.
  • Numește fișierele de test *.spec.js și apelează cleanup() în afterEach.

Soluția de referință

svelte
<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ă)

js
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
js
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:

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>
json
{
  "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

CategorieFiș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țidouble-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.html ca globalThis.htmlContent. Resetează documentul în beforeEach (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ță

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 += 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)

js
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
js
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`:

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();
}
json
{
  "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

CategorieFiș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țiaccepts-blank, case-sensitive-duplicates, keeps-input + mutants.json
  • Aceeași configurare DOM ca la Vanilla JavaScript: resetează documentul din globalThis.htmlContent în beforeEach și montează-l în #app.
  • Biblioteca (lib) de TypeScript nu include DOM.Iterable, așa că descompunerea unui NodeList ([...document.querySelectorAll('li')]) nu se compilează. Folosește în schimb Array.from(...).
  • Importă globalii Jest din @jest/globals și numește fișierele de test *.spec.ts.

Soluția de referință

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 (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)

ts
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
ts
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`:

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();
}
json
{
  "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

CategorieFiș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țidouble-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() și expect(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ță

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 += 1;
  }

  decrement(): void {
    if (this.count > 0) this.count -= 1;
  }

  reset(): void {
    this.count = 0;
  }
}
html
<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)

ts
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
ts
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:

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;
  }
}
json
{
  "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

CategorieFiș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țidouble-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ște fireEvent.click și fă aserțiuni pe noul text.
  • Apelează cleanup în afterEach pentru ca componentele din testele anterioare să nu rămână montate.

Soluția de referință

jsx
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)

js
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
js
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:

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;
json
{
  "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

CategorieFiș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țiaccepts-blank, case-sensitive-duplicates, keeps-input + mutants.json
  • Fișierele de test sunt *.spec.ts, nu .tsx: randează cu React.createElement(App) în loc de JSX.
  • Importă mai întâi @testing-library/jest-dom/jest-globals pentru ca toBeInTheDocument() să fie tipizat și înregistrat.
  • Pentru inputuri controlate, folosește fireEvent.change(input, { target: { value } }).

Soluția de referință

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 (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)

ts
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
ts
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:

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;
json
{
  "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

CategorieFiș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țidouble-step, negative-allowed, reset-guard + mutants.json
  • Importă @testing-library/jest-dom/jest-globals și preia describe, it, expect din @jest/globals.
  • Folosește await pentru fiecare apel fireEvent: Vue actualizează DOM-ul la următorul tick.
  • Numește fișierele de test *.spec.js.

Soluția de referință

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 += 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)

js
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
js
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:

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>
json
{
  "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

CategorieFiș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țiaccepts-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ște await fireEvent.update(input, value): setează valoarea și declanșează evenimentul corect într-un singur apel.

Soluția de referință

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 (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)

ts
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
ts
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:

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>
json
{
  "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

CategorieFiș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țidouble-step, negative-allowed, reset-guard + mutants.json
  • Randează cu o funcție: render(() => <App />). Matcher-ii jest-dom sunt î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ță

jsx
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ă)

jsx
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
jsx
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:

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>
  );
}
json
{
  "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

CategorieFiș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țiaccepts-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ște fireEvent.input(input, { target: { value } }).

Soluția de referință

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 (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ă)

tsx
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
tsx
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:

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>
  );
}
json
{
  "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 ​