|
| 1 | +# Test Generators |
| 2 | + |
| 3 | +A Test Generator is a piece of software that creates a practice exercise's tests from the common [problem specifications](https://github.com/exercism/problem-specifications). |
| 4 | +Some tracks also create tests for concept exercises from a similar track-owned data source. |
| 5 | + |
| 6 | +A Test Generator give us these advantages: |
| 7 | + |
| 8 | +1. They allow adding exercises more quickly without writing much boilerplate code. |
| 9 | +2. Contributors can focus on the **design** of an exercise immediately. |
| 10 | +3. Along the track life, automatic updates of existing tests can lower maintainer workload. |
| 11 | + |
| 12 | +## Contributing to Test Generators |
| 13 | + |
| 14 | +Each language may have its own Test Generator, written in that language. |
| 15 | +It adds code and sometimes files to what [`configlet`](/docs/building/configlet) created / updated. |
| 16 | +The code usually is rendered from template files, written for the tracks preferred templating engine. |
| 17 | +You should find all the details in the tracks contribution docs or a `README` near the test generator. |
| 18 | + |
| 19 | +You should also know: |
| 20 | + |
| 21 | +- what [`configlet create`](/docs/building/configlet/create) or [`configlet sync`](/docs/building/configlet/sync) do. |
| 22 | +- what [`canonical-data.json` in problem specifications](https://github.com/exercism/problem-specifications?tab=readme-ov-file#test-data-canonical-datajson) may provide. |
| 23 | +- why ["creating from scratch" is different from "reproducing for updates"](#from-scratch-vs-updating). |
| 24 | + |
| 25 | +## Creating a Test Generator from scratch |
| 26 | + |
| 27 | +There are various test generators in Exercism's tracks. |
| 28 | +These guidelines are based on the experiences of these tracks. |
| 29 | + |
| 30 | +Even so test generators work very similar, they are very track specific. |
| 31 | +It starts with the choice of the templating engine and ends with additional things they do for each track. |
| 32 | +So a common test generator was not and will not be written. |
| 33 | + |
| 34 | +There were helpful discussions [around the Rust](https://forum.exercism.org/t/advice-for-writing-a-test-generator/7178) and the [JavaScript](https://forum.exercism.org/t/test-generators-for-tracks/10615) test generators. |
| 35 | +The [forum](https://forum.exercism.org/c/exercism/building-exercism/125) also is the best place for seeking additional advice. |
| 36 | + |
| 37 | +### Things to know |
| 38 | + |
| 39 | +- `configlet` cache with a local copy of the problem specifications is stored in a [location depending on the users system](https://nim-lang.org/docs/osappdirs.html#getCacheDir). |
| 40 | + Use `configlet info -o -v d | head -1 | cut -d " " -f 5` to get the location. |
| 41 | + Or fetch data from the problem specifications repository directly (`https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/{{exercise-slug}}/canonical-data.json`) |
| 42 | +- [`canonical-data.json` data structure](https://github.com/exercism/problem-specifications?tab=readme-ov-file#test-data-canonical-datajson) is well documented. There is optional nesting of `cases` arrays in `cases` mixed with actual test cases. |
| 43 | +- The contents of `input` and `expected` test case keys of `canonical-data.json` vary largely. These can include simple scalar values, lambdas in pseudo code, lists of operations to perform on the students code and any other kind of input or result one can imagine. |
| 44 | + |
| 45 | +### From Scratch vs. Updating |
| 46 | + |
| 47 | +There are 2 common tasks a test generator may do, that require separate approaches: |
| 48 | + |
| 49 | +- [Creating tests from scratch](#creating-tests-from-scratch) |
| 50 | +- [Reproducing tests for updates](#reproducing-tests-for-updates) |
| 51 | + |
| 52 | +The reason for this distinction is "designing the exercise" vs. "production-ready code". |
| 53 | + |
| 54 | +When creating tests from scratch the test generator should provide all the information contained in `canonical-data.json` in the resulting files. |
| 55 | +This enables contributors to simply open up the generated test file(s) and find all relevant information interwoven with the tracks boilerplate code. |
| 56 | +They then design the exercise's tests and student facing code based on these files rather than on the original `canonical-data.json`. |
| 57 | +As there is no knowledge of exercise specific things, yet, a one-fits-all template targeting the boilerplate code can be used. |
| 58 | + |
| 59 | +When the exercise is already in production, changes in `canonical-data.json` are rarely a reason to change the design of the exercise. |
| 60 | +So reproducing tests for updates is based on the existing design and should result in production-ready code. |
| 61 | +Much of the additional data presented when creating the exercise from scratch is no longer part of the result. |
| 62 | + |
| 63 | +Instead, very often additional conversion of test case data is required, which is specific to this exercise. |
| 64 | +Most tracks opt for having at least one template per exercise for this. |
| 65 | +This way they can represent all the design choices in that template without complicating things too much for further contribution. |
| 66 | + |
| 67 | +### Creating tests from scratch |
| 68 | + |
| 69 | +This is more productive in the beginning of a tracks life. |
| 70 | +It is way more easy to implement than the "updating" part. |
| 71 | + |
| 72 | +Doing only the bare minimum required for a first usable test generator may already help contributors a lot: |
| 73 | + |
| 74 | +- Read the `canonical-data.json` of the exercise from `configlet` cache or retrieve it from GitHub directly |
| 75 | +- Preserve all data (including `comments`, `description` and `scenarios`) |
| 76 | +- If the tracks testing framework supports no nested test case groups, flatten the nested data structure into a list of test cases |
| 77 | +- Dump the test cases into the one-fits-all boilerplate template(s) |
| 78 | + - Preserve the test case grouping for nested test case groups, e.g. |
| 79 | + - using the test frameworks grouping capability |
| 80 | + - using comments and code folding markers (`{{{`, `}}}`) |
| 81 | + - concatenating group `description` and test case `description` |
| 82 | + - Show all data (including `comments`, `description` and `scenarios`) |
| 83 | + |
| 84 | +```exercism/note |
| 85 | +Don't try to produce perfect production-ready code! |
| 86 | +Dump all data and let the contributor design the exercise from that. |
| 87 | +There is way too much variation in the exercises to handle all in one template. |
| 88 | +``` |
| 89 | + |
| 90 | +There are optional things a test generator might do: |
| 91 | + |
| 92 | +- Provide code for a simple test case (e.g. call a function with `input`, compare result to `expected`) |
| 93 | +- Provide boilerplate code for student code file(s) or additional files required by the track |
| 94 | +- Respect `scenarios` for grouping / test case selection |
| 95 | +- Skip over "reimplemented" test cases (those referred to in a `reimplements` key of another test case) |
| 96 | +- Update `tests.toml` with `include=false` to reflect tests skipped by `scenarios` / `reimplements` |
| 97 | + |
| 98 | +### Reproducing tests for updates |
| 99 | + |
| 100 | +This may become more relevant over track life time. |
| 101 | +It is much harder to implement than the "from scratch" part. |
| 102 | +If you need to invest much effort here, maybe manual maintenance is more efficient. |
| 103 | +Also keep in mind: maintaining the test generator adds to the maintainers workload, too. |
| 104 | + |
| 105 | +```exercism/note |
| 106 | +Choose a flexible and extensible templating engine! |
| 107 | +The test cases vary largely between exercises. |
| 108 | +They include simple scalar values, lambdas in pseudo code, lists of operations to perform on the students code and any other kind of input or result one can imagine. |
| 109 | +``` |
| 110 | + |
| 111 | +Doing the bare minimum required for a usable updating test generator includes: |
| 112 | + |
| 113 | +- Read the `canonical-data.json` of the exercise from `configlet` cache or retrieve it from GitHub directly |
| 114 | +- If the tracks testing framework supports no nested test case groups, flatten the nested data structure into a list of test cases |
| 115 | +- Render the test cases into the exercise specific template(s) located in an exercise's `.meta/` folder |
| 116 | + - Render production-ready code that matches the manually designed exercise |
| 117 | + - Skip over "reimplemented" test cases (those referred to in a `reimplements` key of another test case) |
| 118 | + - Render only test cases selected by `tests.toml` (or another track-specific data source) |
| 119 | + |
| 120 | +There are different strategies for respecting test case changes like "replace always", "replace when forced to", "use `tests.toml` to ignore replaced test cases" (works like a baseline for known test issues). |
| 121 | +None of them is perfect. |
| 122 | + |
| 123 | +```exercism/note |
| 124 | +Don't try to have a versatile one-fits-all template! |
| 125 | +There is way too much variation in the exercises to handle all in one template. |
| 126 | +``` |
| 127 | + |
| 128 | +There are optional things a test generator might do: |
| 129 | + |
| 130 | +- Provide a library of templates and / or extensions to the template engine |
| 131 | +- Maintain or respect another track-specific data source than `tests.toml` |
| 132 | +- Maintain student code file(s) or additional files required by the track |
| 133 | +- Handle `scenarios` for grouping / test case selection |
| 134 | +- Have a check functionality (e.g. to run after `configlet sync`) to detect when updating is required |
0 commit comments