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 run

CategoryWho sees itWhat goes in it
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.
Reference Solution (visible to solver)The learner, read-onlyThe correct implementation. The learner reads it to understand what to test.
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.
MutantsNobody, only their labelsThe buggy variants of the reference solution, one folder per mutant, plus mutants.json.
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 Mutants is one mutant. Its files replace the reference solution files that have the same relative path. For example, Mutants/plus-one/add.js 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 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: Mutants/percent-over-100/src/discount.service.ts replaces src/discount.service.ts.

The mutants.json file ​

mutants.json sits at the root of Mutants and gives every mutant a label and a short description:

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."
  }
}

The keys are the mutant folder names. The learner sees the label and the description in the results panel, whether the mutant was caught or not. Write them as hints about the behavior that breaks, without pasting the buggy line. A mutant without an entry is shown with its folder name.

Rules for mutants ​

The platform checks the structure of 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 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 ​

Click Test in the action bar. On the edit page of a write-tests challenge, Test runs your Author Reference Tests together with the Starter Test Files, the same workspace a learner submits:

  1. First against the reference solution. If any test fails, the run stops and shows the failing tests.
  2. Then against every mutant, one after the other. Each mutant is reported as caught or missed.

Test result showing the tests pass the reference behavior and all 3 hidden buggy variants are caught

Fig. 5 - A successful run: the reference behavior passes and all mutants are caught

Test always uses the files currently open in the editor, including unsaved changes. 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 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 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: the reference behavior passes and all three mutants are caught (Fig. 5). 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. 6).

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

Fig. 6 - 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.