Skip to content

Commit bfbfe5c

Browse files
btmillsmdjermanovicaladdin-add
authored
New: Add only to RuleTester (refs eslint/rfcs#73) (#14677)
* New: Add only to RuleTester (refs eslint/rfcs#73) * Fix variable name typo Co-authored-by: Milos Djermanovic <[email protected]> * Clarify executable name Co-authored-by: 薛定谔的猫 <[email protected]> * Use this in static accessor for consistency Co-authored-by: Milos Djermanovic <[email protected]> * Remove unnecessary spy Co-authored-by: Milos Djermanovic <[email protected]> Co-authored-by: 薛定谔的猫 <[email protected]>
1 parent c2cd7b4 commit bfbfe5c

5 files changed

Lines changed: 346 additions & 14 deletions

File tree

Makefile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ target.mocha = () => {
544544

545545
echo("Running unit tests");
546546

547-
lastReturn = exec(`${getBinFile("nyc")} -- ${MOCHA} -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`);
547+
lastReturn = exec(`${getBinFile("nyc")} -- ${MOCHA} --forbid-only -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`);
548548
if (lastReturn.code !== 0) {
549549
errors++;
550550
}

docs/developer-guide/nodejs-api.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,7 @@ A test case is an object with the following properties:
12561256
* `code` (string, required): The source code that the rule should be run on
12571257
* `options` (array, optional): The options passed to the rule. The rule severity should not be included in this list.
12581258
* `filename` (string, optional): The filename for the given case (useful for rules that make assertions about filenames).
1259+
* `only` (boolean, optional): Run this case exclusively for debugging in supported test frameworks.
12591260

12601261
In addition to the properties above, invalid test cases can also have the following properties:
12611262

@@ -1355,10 +1356,13 @@ ruleTester.run("my-rule-for-no-foo", rule, {
13551356
`RuleTester` depends on two functions to run tests: `describe` and `it`. These functions can come from various places:
13561357

13571358
1. If `RuleTester.describe` and `RuleTester.it` have been set to function values, `RuleTester` will use `RuleTester.describe` and `RuleTester.it` to run tests. You can use this to customize the behavior of `RuleTester` to match a test framework that you're using.
1358-
1. Otherwise, if `describe` and `it` are present as globals, `RuleTester` will use `global.describe` and `global.it` to run tests. This allows `RuleTester` to work when using frameworks like [Mocha](https://mochajs.org/) without any additional configuration.
1359-
1. Otherwise, `RuleTester#run` will simply execute all of the tests in sequence, and will throw an error if one of them fails. This means you can simply execute a test file that calls `RuleTester.run` using `node`, without needing a testing framework.
13601359

1361-
`RuleTester#run` calls the `describe` function with two arguments: a string describing the rule, and a callback function. The callback calls the `it` function with a string describing the test case, and a test function. The test function will return successfully if the test passes, and throw an error if the test fails. (Note that this is the standard behavior for test suites when using frameworks like [Mocha](https://mochajs.org/); this information is only relevant if you plan to customize `RuleTester.it` and `RuleTester.describe`.)
1360+
If `RuleTester.itOnly` has been set to a function value, `RuleTester` will call `RuleTester.itOnly` instead of `RuleTester.it` to run cases with `only: true`. If `RuleTester.itOnly` is not set but `RuleTester.it` has an `only` function property, `RuleTester` will fall back to `RuleTester.it.only`.
1361+
1362+
2. Otherwise, if `describe` and `it` are present as globals, `RuleTester` will use `global.describe` and `global.it` to run tests and `global.it.only` to run cases with `only: true`. This allows `RuleTester` to work when using frameworks like [Mocha](https://mochajs.org/) without any additional configuration.
1363+
3. Otherwise, `RuleTester#run` will simply execute all of the tests in sequence, and will throw an error if one of them fails. This means you can simply execute a test file that calls `RuleTester.run` using `Node.js`, without needing a testing framework.
1364+
1365+
`RuleTester#run` calls the `describe` function with two arguments: a string describing the rule, and a callback function. The callback calls the `it` function with a string describing the test case, and a test function. The test function will return successfully if the test passes, and throw an error if the test fails. The signature for `only` is the same as `it`. `RuleTester` calls either `it` or `only` for every case even when some cases have `only: true`, and the test framework is responsible for implementing test case exclusivity. (Note that this is the standard behavior for test suites when using frameworks like [Mocha](https://mochajs.org/); this information is only relevant if you plan to customize `RuleTester.describe`, `RuleTester.it`, or `RuleTester.itOnly`.)
13621366

13631367
Example of customizing `RuleTester`:
13641368

docs/developer-guide/unit-tests.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,29 @@ This automatically starts Mocha and runs all tests in the `tests` directory. You
1010

1111
## Running Individual Tests
1212

13-
If you want to quickly run just one test, you can do so by running Mocha directly and passing in the filename. For example:
13+
If you want to quickly run just one test file, you can do so by running Mocha directly and passing in the filename. For example:
1414

1515
npm run test:cli tests/lib/rules/no-wrap-func.js
1616

17-
Running individual tests is useful when you're working on a specific bug and iterating on the solution. You should be sure to run `npm test` before submitting a pull request.
17+
If you want to run just one or a subset of `RuleTester` test cases, add `only: true` to each test case or wrap the test case in `RuleTester.only(...)` to add it automatically:
18+
19+
```js
20+
ruleTester.run("my-rule", myRule, {
21+
valid: [
22+
RuleTester.only("const valid = 42;"),
23+
// Other valid cases
24+
],
25+
invalid: [
26+
{
27+
code: "const invalid = 42;",
28+
only: true,
29+
},
30+
// Other invalid cases
31+
]
32+
})
33+
```
34+
35+
Running individual tests is useful when you're working on a specific bug and iterating on the solution. You should be sure to run `npm test` before submitting a pull request. `npm test` uses Mocha's `--forbid-only` option to prevent `only` tests from passing full test runs.
1836

1937
## More Control on Unit Testing
2038

lib/rule-tester/rule-tester.js

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const espreePath = require.resolve("espree");
7171
* @property {{ [name: string]: any }} [parserOptions] Options for the parser.
7272
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
7373
* @property {{ [name: string]: boolean }} [env] Environments for the test case.
74+
* @property {boolean} [only] Run only this test case or the subset of test cases with this property.
7475
*/
7576

7677
/**
@@ -86,6 +87,7 @@ const espreePath = require.resolve("espree");
8687
* @property {{ [name: string]: any }} [parserOptions] Options for the parser.
8788
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
8889
* @property {{ [name: string]: boolean }} [env] Environments for the test case.
90+
* @property {boolean} [only] Run only this test case or the subset of test cases with this property.
8991
*/
9092

9193
/**
@@ -121,7 +123,8 @@ const RuleTesterParameters = [
121123
"filename",
122124
"options",
123125
"errors",
124-
"output"
126+
"output",
127+
"only"
125128
];
126129

127130
/*
@@ -282,6 +285,7 @@ function wrapParser(parser) {
282285
// default separators for testing
283286
const DESCRIBE = Symbol("describe");
284287
const IT = Symbol("it");
288+
const IT_ONLY = Symbol("itOnly");
285289

286290
/**
287291
* This is `it` default handler if `it` don't exist.
@@ -400,6 +404,46 @@ class RuleTester {
400404
this[IT] = value;
401405
}
402406

407+
/**
408+
* Adds the `only` property to a test to run it in isolation.
409+
* @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
410+
* @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
411+
*/
412+
static only(item) {
413+
if (typeof item === "string") {
414+
return { code: item, only: true };
415+
}
416+
417+
return { ...item, only: true };
418+
}
419+
420+
static get itOnly() {
421+
if (typeof this[IT_ONLY] === "function") {
422+
return this[IT_ONLY];
423+
}
424+
if (typeof this[IT] === "function" && typeof this[IT].only === "function") {
425+
return Function.bind.call(this[IT].only, this[IT]);
426+
}
427+
if (typeof it === "function" && typeof it.only === "function") {
428+
return Function.bind.call(it.only, it);
429+
}
430+
431+
if (typeof this[DESCRIBE] === "function" || typeof this[IT] === "function") {
432+
throw new Error(
433+
"Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
434+
"See https://eslint.org/docs/developer-guide/nodejs-api#customizing-ruletester for more."
435+
);
436+
}
437+
if (typeof it === "function") {
438+
throw new Error("The current test framework does not support exclusive tests with `only`.");
439+
}
440+
throw new Error("To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.");
441+
}
442+
443+
static set itOnly(value) {
444+
this[IT_ONLY] = value;
445+
}
446+
403447
/**
404448
* Define a rule for one particular run of tests.
405449
* @param {string} name The name of the rule to define.
@@ -891,23 +935,29 @@ class RuleTester {
891935
RuleTester.describe(ruleName, () => {
892936
RuleTester.describe("valid", () => {
893937
test.valid.forEach(valid => {
894-
RuleTester.it(sanitize(typeof valid === "object" ? valid.code : valid), () => {
895-
testValidTemplate(valid);
896-
});
938+
RuleTester[valid.only ? "itOnly" : "it"](
939+
sanitize(typeof valid === "object" ? valid.code : valid),
940+
() => {
941+
testValidTemplate(valid);
942+
}
943+
);
897944
});
898945
});
899946

900947
RuleTester.describe("invalid", () => {
901948
test.invalid.forEach(invalid => {
902-
RuleTester.it(sanitize(invalid.code), () => {
903-
testInvalidTemplate(invalid);
904-
});
949+
RuleTester[invalid.only ? "itOnly" : "it"](
950+
sanitize(invalid.code),
951+
() => {
952+
testInvalidTemplate(invalid);
953+
}
954+
);
905955
});
906956
});
907957
});
908958
}
909959
}
910960

911-
RuleTester[DESCRIBE] = RuleTester[IT] = null;
961+
RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null;
912962

913963
module.exports = RuleTester;

0 commit comments

Comments
 (0)