Skip to content

Write-tests challenges ​

In a classic code challenge the learner writes the implementation and your tests check it. A write-tests challenge flips the roles: you provide a correct implementation, and the learner writes the unit tests for it. 🧪

To prove that the learner's tests are actually good, DojoCode runs them twice:

  1. Against your reference solution, the correct implementation. Every test must pass.
  2. Against every mutant, a copy of the reference solution with one small, realistic bug you planted on purpose. At least one test must fail on each mutant.

A mutant that makes at least one test fail is caught. The learner completes the challenge when their tests pass on the reference solution and catch every mutant. This technique is known as mutation testing, and it is the most honest way to measure whether a test suite really pins down behavior.

When to use this mode

Write-tests challenges are a great fit for teaching testing itself: edge cases, boundaries, error handling, and testing UI components or HTTP endpoints. Learners read real code and have to think about how it could break.

Supported templates ​

The Write tests mode is available for these templates:

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

A complete, verified example for every template is available in Write-tests examples by template.

Creating a write-tests draft ​

  1. Open My Challenges and click NEW DRAFT.
  2. Enter a Title and select a template that supports the mode.
  3. Choose Write tests in the mode selector under the title (Fig. 1). The selector only appears for supported templates. Selecting another template switches the draft back to Write Solution.
  4. Click Create draft.

Create Code Challenge modal with the Write tests mode selected for the NodeJS template

Fig. 1 - The mode selector in the Create Code Challenge modal, with Write tests selected for NodeJS

The mode is chosen once

The mode is set when the draft is created and cannot be changed later. To turn a classic challenge into a write-tests challenge, create a new draft in Write tests mode.

The new draft does not start empty. Every supported template ships a small working example: a starter test, a reference solution, an author reference suite and two mutants. You can run it immediately and then replace it with your own challenge.

The edit page ​

A write-tests draft uses the same edit page as a classic challenge, with different file categories in the Files panel (Fig. 2).

Edit page of a Node.js write-tests draft with the Files panel, the editor and the results panel

Fig. 2 - The edit page of the Node.js Add Numbers: Write the Tests challenge after a successful Test hidden bugs run

The categories are listed in the order you build the challenge:

CategoryWho sees itWhat goes in it
Reference Solution (visible to solver)The learner, read-onlyThe correct implementation. The learner reads it to understand what to test.
Starter Test FilesThe learner, editableThe test file the learner starts from, plus supporting files such as the Run entry point (main.js, main.py) or the web entry files of browser templates.
Hidden Bugs (mutants)NobodyThe buggy copies of the reference solution, one folder per mutant, plus mutants.json. Learners only see a mutant's label and description as described in What the learner sees.
Author Reference Tests (publish check)Only youYour own test suite. It proves the challenge is solvable: it must pass on the reference solution and catch every mutant.
Initial TestsNobodyNot used in write-tests mode. It only appears on drafts where the template starter filled it, and you can leave it as it is.

The file flags from classic challenges still apply to Starter Test Files: hidden, read-only, initially opened and main file. See Create a code challenge for their meaning.

The reference solution always wins

When a learner's workspace contains a file at the same path as a reference solution file, the platform ignores the learner's copy and runs the real reference solution. The learner cannot change the implementation under test.

Mutants ​

Every top-level folder inside Hidden Bugs (mutants) is one mutant. Its files replace the reference solution files that have the same relative path. For example, plus-one/add.js in Hidden Bugs (mutants) replaces add.js from the reference solution while that mutant runs. Every other file of the reference solution stays the same.

Files panel with the Hidden Bugs (mutants) category expanded and mutants.json open in the editor

Fig. 3 - Three mutant folders and the mutants.json file that labels them

A mutant file is a full copy of the reference file with one subtle change (Fig. 4):

A mutant file that adds the absolute values of the two numbers instead of the numbers themselves

Fig. 4 - The negatives-as-positive mutant keeps the same API and the same validation, and only adds absolute values to the result

For templates with a src/ folder, the mutant folder mirrors it: percent-over-100/src/discount.service.ts in Hidden Bugs (mutants) replaces src/discount.service.ts.

The mutants.json file ​

mutants.json sits at the root of Hidden Bugs (mutants) and gives every mutant a label and a description:

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

The keys are the mutant folder names.

  • The label is the hint. It names the behavior the learner should test (Negative numbers), never the bug itself: Negatives handled wrong would give the answer away.
  • The description states the exact bug. The learner reads it only after catching the mutant, as the explanation of what their test protects.

A mutant without an entry uses its folder name as its label, so always add an entry for every mutant.

What the learner sees ​

Learners never see the mutant folders or their files. In the results panel of Verify tests and Submit, a mutant shows differently depending on whether their tests caught it:

Missed mutantCaught mutant
NameHidden bug #NThe label
DetailsA reminder that all their tests still pass on this buggy copy, and the label as HintThe description, and Caught by with the names of their tests that failed on it

The hidden parts are never sent to the learner's browser, so they cannot be read from the network requests either. As the author, you always see every label and description when you run Test hidden bugs on the edit page.

Rules for mutants ​

The platform checks the structure of Hidden Bugs (mutants) before running anything:

  • There is at least one mutant folder, and every mutant folder contains at least one file.
  • The only file allowed directly at the root of Hidden Bugs (mutants) is mutants.json.
  • Every mutant file must replace an existing reference solution file. A mutant cannot add new files.

Writing good mutants ​

  • One realistic bug per mutant. Off-by-one boundaries, a wrong operator, a missing validation, swapped branches, a wrong status code, a missing disabled state. Each mutant should teach one testing lesson.
  • Keep the public API identical. Same exports, same function signatures, same component markup structure. A mutant that fails to compile or crashes on import is caught by any test, so it teaches nothing.
  • Avoid endless loops. A mutant run that times out is never counted as caught.
  • Make every mutant catchable. A mutant that behaves exactly like the reference solution for every input (an equivalent mutant) can never be caught, and the challenge cannot be completed. Your author reference tests will reveal it before you publish.
  • Start with 2 to 4 mutants. They should cover different behaviors, such as the happy path, a boundary and an error case.

Testing your challenge ​

The action bar of a write-tests challenge has two test buttons. Both run your Author Reference Tests together with the Starter Test Files, the same workspace a learner submits, and both use the files currently open in the editor, including unsaved changes:

ButtonWhat it runs
TestYour tests against the reference solution only. Every test must pass.
Test hidden bugsYour tests against every mutant, one after the other. Each mutant is reported as caught or missed.

Click Test first. The results panel shows the result of each test, as in a classic challenge (Fig. 5).

Test result: every author reference test and the starter test pass on the correct solution

Fig. 5 - Test runs the starter test and the author reference suite on the reference solution: all four tests pass

Once every test passes, click Test hidden bugs. The results panel shows the same report a learner sees: the summary, the verdict and one line for each mutant with the names of the tests that caught it (Fig. 6). The tests still run on the reference solution first, so when one of them fails there, the run stops and shows the failing tests instead.

Test hidden bugs result: all 3 hidden bugs are caught, each with the tests that caught it

Fig. 6 - A successful Test hidden bugs run: each mutant is caught, with the test that caught it

The Test All button of classic challenges does not exist in this mode.

Keep the starter test honest

Because the starter test runs next to your reference suite, it must pass on the reference solution too. A good starter test covers one obvious case and leaves most mutants for the learner to catch.

Publishing ​

Before publishing, the challenge needs a starter test file, a reference solution, an author reference suite and at least one mutant that contains a file.

When you click Publish, every template variation of the challenge is validated in the background:

  1. The mutant rules above are checked.
  2. Your author reference tests (with the starter files) run against the reference solution and every mutant of that variation.

The challenge is published only if, for every variation, the reference run is green and every mutant is caught. Otherwise the validation window shows the failing variation with the same report as the Test hidden bugs button.

Adding other templates ​

Use the + button next to the template selector to add a variation in another language. Only templates that support write-tests challenges can be added. The new variation starts from that template's write-tests example, with its own starter test, reference solution, author reference suite and mutants.

Let the AI assistant help

The AI Chat on the edit page knows the write-tests conventions of every supported template. Ask it to draft mutants for your reference solution or to strengthen your author reference tests, then run Test and Test hidden bugs to check the result.

Walkthrough: Node.js ​

The Add Numbers: Write the Tests challenge is a complete, small example. The function is trivial, so the whole challenge is about the tests: negative numbers, the result itself and invalid input.

Reference solution (visible to solver)

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 };

Starter test, the file the learner opens first. It covers the obvious case and leaves the rest to the learner.

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.
});

Author reference tests (publish check). One test per behavior that a mutant breaks:

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

Mutants. Each folder holds a buggy copy of add.js with one change:

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 };

Notice how each mutant maps to one test of the reference suite:

MutantWhat changesCaught by
plus-oneEvery result is one too highAny exact sum, even the starter test add(2, 3)
negatives-as-positiveadd(-2, -3) returns 5A sum with a negative operand
strings-convertedadd('1', 2) returns 3 instead of throwingexpect(() => add('1', 2)).toThrow(TypeError)

Click Test: every test passes on the correct solution (Fig. 5). Then click Test hidden bugs: all three mutants are caught (Fig. 6). The starter test alone only catches plus-one, which is exactly the gap the learner has to close.

Walkthrough: Python ​

The Python variation of the same challenge has the same structure, with pytest (Fig. 7).

Edit page of the Python variation of Add Numbers after a successful Test hidden bugs run

Fig. 7 - The Python variation with the negatives-as-positive mutant open and all 3 mutants caught

Reference solution (visible to solver)

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

Starter test. Tests import the solution through the challenge package:

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.

Author reference tests (publish check)

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)

Mutants. The same three bugs, written in Python. The variation reuses the same 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 stays the Run entry point. It imports without the package prefix (from add import add), so learners can print values while they explore the code.