Skip to content

Provocări de tip scriere teste ​

Într-o provocare de cod clasică, cursantul scrie implementarea, iar testele tale o verifică. O provocare de tip scriere teste inversează rolurile: tu oferi o implementare corectă, iar cursantul scrie testele unitare pentru aceasta. 🧪

Pentru a dovedi că testele cursantului sunt cu adevărat bune, DojoCode le rulează de două ori:

  1. Pe baza soluției de referință, adică implementarea corectă. Fiecare test trebuie să treacă.
  2. Pe baza fiecărui mutant, o copie a soluției de referință care conține un mic bug realist introdus de tine intenționat. Cel puțin un test trebuie să eșueze pentru fiecare mutant.

Un mutant care face ca cel puțin un test să eșueze este prins. Cursantul finalizează provocarea atunci când testele sale trec pe soluția de referință și prind fiecare mutant. Această tehnică este cunoscută sub numele de mutation testing și este cel mai onest mod de a măsura dacă o suită de teste definește cu adevărat comportamentul codului.

Când să folosești acest mod

Provocările de tip scriere teste sunt ideale pentru a preda testarea în sine: cazuri limită, valori de graniță, gestionarea erorilor și testarea componentelor UI sau a endpoint-urilor HTTP. Cursanții citesc cod real și trebuie să se gândească la modul în care acesta s-ar putea defecta.

Șabloane acceptate ​

Modul Scriere teste este disponibil pentru următoarele șabloane:

GrupȘabloaneTest runner
TerminalNode.js, Node.js cu TypeScriptJest
TerminalPythonpytest
Backend cu API testerNestJSVitest + @nestjs/testing
Backend cu API testerFastify, HonoVitest
BrowserSvelte, Vanilla JavaScript, Vanilla TypeScript, Angular, React, React cu TypeScript, Vue, Vue cu TypeScriptJest + Testing Library
BrowserSolidJS, SolidJS cu TypeScriptVitest + Testing Library

Un exemplu complet și verificat pentru fiecare șablon este disponibil în Exemple de scriere teste după șablon.

Crearea unei ciorne de tip scriere teste ​

  1. Deschide My Challenges și dă clic pe NEW DRAFT.
  2. Introdu un Titlu și selectează un șablon care acceptă acest mod.
  3. Alege Write tests în selectorul de mod de sub titlu (Fig. 1). Selectorul apare doar pentru șabloanele acceptate. Selectarea unui alt șablon comută ciorna înapoi la Write Solution.
  4. Dă clic pe Create draft.

Modalul Create Code Challenge cu modul Write tests selectat pentru șablonul NodeJS

Fig. 1 - Selectorul de mod din modalul Create Code Challenge, cu Write tests selectat pentru NodeJS

Modul se alege o singură dată

Modul este stabilit atunci când este creată ciorna și nu mai poate fi schimbat ulterior. Pentru a transforma o provocare clasică într-o provocare de tip scriere teste, creează o ciornă nouă în modul Write tests.

Noua ciornă nu pornește goală. Fiecare șablon acceptat vine cu un mic exemplu funcțional: un test de pornire, o soluție de referință, o suită de referință a autorului și doi mutanți. Îl poți rula imediat și apoi îl poți înlocui cu propria ta provocare.

Pagina de editare ​

O ciornă de tip scriere teste folosește aceeași pagină de editare ca o provocare clasică, dar cu categorii diferite de fișiere în panoul Files (Fig. 2).

Pagina de editare a unei ciorne de tip scriere teste Node.js cu panoul Files, editorul și panoul de rezultate

Fig. 2 - Pagina de editare a provocării Node.js Add Numbers: Write the Tests după o rulare cu succes a Test-ului

CategorieCine o vedeCe conține
Starter Test FilesCursantul, editabilFișierul de test de la care pornește cursantul, plus fișierele de suport, cum ar fi punctul de pornire pentru Run (main.js, main.py) sau fișierele de intrare web pentru șabloanele de browser.
Reference Solution (visible to solver)Cursantul, doar citireImplementarea corectă. Cursantul o citește pentru a înțelege ce trebuie să testeze.
Author Reference Tests (publish check)Doar tuSuita ta de teste. Aceasta dovedește că provocarea poate fi rezolvată: trebuie să treacă pe soluția de referință și să prindă fiecare mutant.
MutantsNimeni, doar etichetele lorVariantele cu bug-uri ale soluției de referință, un folder pentru fiecare mutant, plus mutants.json.
Initial TestsNimeniNu se utilizează în modul scriere teste. Apare doar pe ciornele unde șablonul de pornire a completat-o și o poți lăsa așa cum este.

Flag-urile de fișier din provocările clasice se aplică în continuare pentru Starter Test Files: hidden, read-only, initially opened și main file. Vezi Creează o provocare de cod pentru semnificația lor.

Soluția de referință are întotdeauna prioritate

Când spațiul de lucru al unui cursant conține un fișier pe aceeași cale cu un fișier al soluției de referință, platforma ignoră copia cursantului și rulează soluția de referință reală. Cursantul nu poate modifica implementarea testată.

Mutanți ​

Fiecare folder de nivel superior din Mutants reprezintă un mutant. Fișierele sale înlocuiesc fișierele soluției de referință care au aceeași cale relativă. De exemplu, Mutants/plus-one/add.js înlocuiește add.js din soluția de referință în timpul rulării acelui mutant. Toate celelalte fișiere ale soluției de referință rămân neschimbate.

Panoul Files cu categoria Mutants extinsă și mutants.json deschis în editor

Fig. 3 - Trei foldere de mutanți și fișierul mutants.json care le etichetează

Un fișier mutant este o copie completă a fișierului de referință cu o singură modificare subtilă (Fig. 4):

Un fișier mutant care adună valorile absolute ale celor două numere în loc de numerele în sine

Fig. 4 - Mutantul negatives-as-positive păstrează același API și aceeași validare și doar adaugă valorile absolute la rezultat

Pentru șabloanele cu un folder src/, folderul mutant îl oglindește: Mutants/percent-over-100/src/discount.service.ts înlocuiește src/discount.service.ts.

Fișierul mutants.json ​

mutants.json se află în rădăcina folderului Mutants și oferă fiecărui mutant o etichetă și o scurtă descriere:

json
{
  "plus-one": {
    "label": "Off by one",
    "description": "The result is one more than it should be."
  },
  "negatives-as-positive": {
    "label": "Negatives handled wrong",
    "description": "Negative numbers are added as if they were positive."
  },
  "strings-converted": {
    "label": "Invalid input accepted",
    "description": "A string is converted to a number instead of being rejected."
  }
}

Cheile sunt numele folderelor de mutanți. Cursantul vede eticheta și descrierea în panoul de rezultate, indiferent dacă mutantul a fost prins sau nu. Scrie-le ca indicii despre comportamentul care se strică, fără a introduce linia cu bug-ul. Un mutant fără o intrare în fișier este afișat cu numele folderului său.

Reguli pentru mutanți ​

Platforma verifică structura folderului Mutants înainte de a rula orice:

  • Există cel puțin un folder de mutant și fiecare folder de mutant conține cel puțin un fișier.
  • Singurul fișier permis direct în rădăcina folderului Mutants este mutants.json.
  • Fiecare fișier mutant trebuie să înlocuiască un fișier existent din soluția de referință. Un mutant nu poate adăuga fișiere noi.

Cum să scrii mutanți buni ​

  • Un singur bug realist per mutant. Limite de tip off-by-one, un operator greșit, o validare lipsă, ramuri inversate, un cod de stare greșit, o stare disabled lipsă. Fiecare mutant ar trebui să predea o lecție de testare.
  • Păstrează API-ul public identic. Aceleași exporturi, aceleași semnături de funcții, aceeași structură de markup a componentelor. Un mutant care nu se compilează sau crapă la import este prins de orice test, deci nu oferă nicio învățătură.
  • Evită buclele infinite. O rulare de mutant care expiră (timeout) nu este niciodată considerată ca fiind prinsă.
  • Asigură-te că fiecare mutant poate fi prins. Un mutant care se comportă exact ca soluția de referință pentru orice input (un mutant echivalent) nu poate fi prins niciodată, iar provocarea nu poate fi finalizată. Testele tale de referință ca autor îl vor dezvălui înainte de a publica.
  • Începe cu 2 până la 4 mutanți. Aceștia ar trebui să acopere comportamente diferite, cum ar fi calea fericită (happy path), o limită și un caz de eroare.

Testarea provocării tale ​

Apasă pe Test în bara de acțiuni. Pe pagina de editare a unei provocări de tip scriere de teste (write-tests), Test rulează testele de referință ale autorului împreună cu fișierele de test de pornire, același spațiu de lucru pe care îl trimite un cursant:

  1. Mai întâi pe soluția de referință. Dacă un test eșuează, rularea se oprește și afișează testele care au eșuat.
  2. Apoi pe fiecare mutant, unul după altul. Fiecare mutant este raportat ca fiind prins sau ratat.

Rezultatul testului arătând că testele trec de comportamentul de referință și toate cele 3 variante cu erori ascunse sunt prinse

Fig. 5 - O rulare reușită: comportamentul de referință trece și toți mutanții sunt prinși

Test folosește întotdeauna fișierele deschise în prezent în editor, inclusiv modificările nesalvate. Butonul Test All din provocările clasice nu există în acest mod.

Păstrează testul de pornire onest

Deoarece testul de pornire rulează alături de suita ta de referință, acesta trebuie să treacă și pe soluția de referință. Un test de pornire bun acoperă un singur caz evident și lasă majoritatea mutanților de prins pentru cursant.

Publicarea ​

Înainte de publicare, provocarea are nevoie de un fișier de test de pornire, o soluție de referință, o suită de referință a autorului și cel puțin un mutant care conține un fișier.

Când apeși pe Publish, fiecare variație de șablon a provocării este validată în fundal:

  1. Sunt verificate regulile pentru mutanți de mai sus.
  2. Testele tale de referință ca autor (cu fișierele de pornire) rulează pe soluția de referință și pe fiecare mutant al acelei variații.

Provocarea este publicată doar dacă, pentru fiecare variație, rularea de referință este verde și fiecare mutant este prins. În caz contrar, fereastra de validare arată variația care a eșuat cu același raport ca și butonul Test.

Adăugarea altor șabloane ​

Folosește butonul + de lângă selectorul de șabloane pentru a adăuga o variație în altă limbă. Pot fi adăugate doar șabloanele care acceptă provocări de tip scriere de teste. Noua variație pornește de la exemplul de scriere de teste al acelui șablon, cu propriul test de pornire, soluție de referință, suită de referință a autorului și mutanți.

Lasă Asistentul AI să te ajute

Chatul AI (AI Chat) de pe pagina de editare cunoaște convențiile de scriere a testelor pentru fiecare șablon acceptat. Roagă-l să creeze schițe de mutanți pentru soluția ta de referință sau să îți consolideze testele de referință ca autor, apoi rulează Test pentru a verifica rezultatul.

Ghid pas cu pas: Node.js ​

Provocarea Add Numbers: Write the Tests este un exemplu complet și de mici dimensiuni. Funcția este trivială, așa că întreaga provocare se referă la teste: numere negative, rezultatul în sine și inputul nevalid.

Soluția de referință (vizibilă pentru rezolvitor)

js
function add(a, b) {
  if (typeof a !== "number" || typeof b !== "number") {
    throw new TypeError("add expects two numbers");
  }

  return a + b;
}

module.exports = { add };

Testul de pornire, fișierul pe care cursantul îl deschide primul. Acoperă cazul evident și îi lasă restul cursantului.

js
const { describe, it, expect } = require('@jest/globals');
const { add } = require('./add');

describe('add', () => {
  it('adds two positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  // Add tests until every hidden bug is caught.
});

Teste de referință ale autorului (verificare la publicare). Un test pentru fiecare comportament pe care un mutant îl strică:

js
const { describe, it, expect } = require('@jest/globals');
const { add } = require('./add');

describe('add (reference suite)', () => {
  it('adds two positive numbers', () => {
    expect(add(1, 2)).toBe(3);
  });

  it('adds negative numbers correctly', () => {
    expect(add(-2, -3)).toBe(-5);
    expect(add(-2, 5)).toBe(3);
  });

  it('throws a TypeError when an argument is not a number', () => {
    expect(() => add('1', 2)).toThrow(TypeError);
    expect(() => add(1, undefined)).toThrow(TypeError);
  });
});

Mutanți. Fiecare dosar conține o copie cu erori a fișierului add.js cu o singură modificare:

js
function add(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('add expects two numbers');
  }

  return a + b + 1;
}

module.exports = { add };
js
function add(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('add expects two numbers');
  }

  return Math.abs(a) + Math.abs(b);
}

module.exports = { add };
js
function add(a, b) {
  return Number(a) + Number(b);
}

module.exports = { add };

Observă cum fiecare mutant se asociază cu un test din suita de referință:

MutantCe se modificăPrins de
plus-oneFiecare rezultat este cu unu mai mareOrice sumă exactă, chiar și testul de pornire add(2, 3)
negatives-as-positiveadd(-2, -3) returnează 5O sumă cu un operand negativ
strings-convertedadd('1', 2) returnează 3 în loc să arunce o eroareexpect(() => add('1', 2)).toThrow(TypeError)

Apasă pe Test: comportamentul de referință trece și toți cei trei mutanți sunt prinși (Fig. 5). Doar testul de pornire îl prinde pe cel plus-one, ceea ce este exact lacuna pe care cursantul trebuie să o acopere.

Ghid pas cu pas: Python ​

Variația Python a aceleiași provocări are aceeași structură, cu pytest (Fig. 6).

Pagina de editare a variației Python a provocării Add Numbers după o rulare reușită a testului

Fig. 6 - Variația Python cu mutantul negatives-as-positive deschis și toți cei 3 mutanți prinși

Soluția de referință (vizibilă pentru rezolvitor)

python
def add(a, b):
    """Return a + b. Both arguments must be numbers."""
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("add expects two numbers")

    return a + b

Testul de pornire. Testele importă soluția prin intermediul pachetului challenge:

python
from pytest import mark as m
from challenge.add import add


@m.describe("add")
class TestAdd:
    @m.it("Adds two positive numbers")
    def test_positive(self):
        assert add(2, 3) == 5

    # Add tests until every hidden bug is caught.

Teste de referință ale autorului (verificare la publicare)

python
import pytest
from pytest import mark as m
from challenge.add import add


@m.describe("add (reference suite)")
class TestAddReference:
    @m.it("Adds two positive numbers")
    def test_positive(self):
        assert add(1, 2) == 3

    @m.it("Adds negative numbers correctly")
    def test_negative(self):
        assert add(-2, -3) == -5
        assert add(-2, 5) == 3

    @m.it("Raises a TypeError when an argument is not a number")
    def test_not_a_number(self):
        with pytest.raises(TypeError):
            add("1", 2)
        with pytest.raises(TypeError):
            add(1, None)

Mutanți. Aceleași trei bug-uri, scrise în Python. Variația reutilizează același mutants.json.

python
def add(a, b):
    """Return a + b. Both arguments must be numbers."""
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("add expects two numbers")

    return a + b + 1
python
def add(a, b):
    """Return a + b. Both arguments must be numbers."""
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("add expects two numbers")

    return abs(a) + abs(b)
python
def add(a, b):
    """Return a + b. Both arguments must be numbers."""
    return float(a) + float(b)

main.py rămâne punctul de pornire pentru Run. Acesta importă fără prefixul pachetului (from add import add), astfel încât cursanții să poată afișa valori în timp ce explorează codul.

Pagini similare ​