Risei is a new way to write unit tests that's easier and faster, more dependable, and keeps your tests from standing in the way of redesigns.
Risei does all this by replacing coded tests with simple declarative syntax.
Here are two example tests, in which SortModel.countSort() is being tested using the inputs from .in and the expected output from .out:
 
Here are the example test results for those two tests:
 
Test runs have a title bar so the starting point is easy to find:
 
And they have a summary bar at the bottom:
 
Risei's major features are now complete, but new enhancements or fixes may appear from time to time.
The latest release, 3.3.2, adds async capabilities to the .from feature.
Check out the full list of changes.
Install Risei for development time only:
npm install --save-dev risei
Ensure that package.json specifies ECMAScript modules:
"type": "module"
And add Risei's metadata to package.json:
"risei": {
  "tests": "**.rt.js"
}
To test Risei itself, you clone it from a parallel repository, install dependencies with npm, and run its self-tests.
git clone https://gitlab.com/riseimaker/risei-public.git risei cd risei npm install npm update npm run mixedtest
mixedtest:   (Seen in example.)   Runs all mocha / chai unit tests, then all of Risei's unit tests for itself.alltest:   Runs everything mixedtest does, plus trial tests (Risei tests mostly of other code, as a sort of double-check).test / npm test (which runs only Risei's unit tests of itself).You write tests in .rt.js files like this:
import ATestSource from "risei/ATestSource";
import ClassToTest from "ClassToTest.js";
export class SomeTests extends ATestSource {
    tests = [ ... ];
}
Write individual tests as plain JavaScript objects with Risei's simple syntax:
tests = [ ...
    { on: ContainerClass, with: [ "a", "b", "c" ],    // Target class and constructor args.
      of: "doesContain",                              // Target method name.
      for: "When the arg is present, returns true.",  // Description of test.
      in: [ "c" ],                                    // Inputs to method.
      out: true },                                    // Expected output.
... ];
.on, .with, .of, .for, .in, and .out defined..in or .with.Asynchronous / awaitable code can tested with no changes at all to this syntax.
If you use.plusor more advanced features like.from, you define asynchronous / awaitable
functions usingasyncandawaitnormally. (See later sections for more details.)
Once you have some tests written, you can run them manually:
node ./node_modules/risei/index.js
Or write a script in package.json that does the same:
"scripts": {
  "test": "node ./node_modules/risei/index.js"
}
And then run that script:
npm test
You can write more tests with less syntax by stating repeated test properties just once, and letting them collapse forward across subsequent tests.
Risei collapses values together until it has a full test to run, as seen in this typical example:
{ on: ContainerClass, with: [ "a", "b", "c" ] },                     // Following tests are of this class.
{ of: "doesContain" },                                               // Following tests are of this method.
{ for: "Returns true when arg present.", in: [ "c" ], out: true },   // First test: now Risei has enough to run on.
{ for: "Returns false when arg absent.", in: [ "d" ], out: false },  // Next test: same method, different test case.
{ of: "countOf" },                                                   // Change of tested method.  Method-related props are wiped out.
...
{ on: SortingClass, with: [ ] },                                     // Change of tested class.  All existing props are wiped out.
Every property collapses forward until it is set to something else or wiped out with a target shift:
.on (the target class) wipes out all other test properties..of (the target method) wipes out all except .on, .with, and .plus (described later).[]..just:You may want to pre-set a property for an upcoming group of tests, even when fully defined tests already exist.   However, this creates an accidental 
 extra test (which often fails), because Risei always collapses old and new values together to make a new fully defined test.
It's easy to avoid this problem with the .just / .only property:
{ on: ContainerClass, with: [ "a", "b", "c", ] },  // A section of tests using these values.
{ ... },
{ ... },
{ just: true, with: [ "t", "u", "v" ] },           // Changing .with without creating an extra test.
{ ... },                                           // Tests using the new .with start here.
{ ... },
.just doesn't have to be true, or any value in particular.   The property just has to be defined.Collapsing forward reuses test property contents directly.   Tests are isolated unless your code mutates its arguments.   If so, the mutated values 
 are used, throwing off test results.
To prevent this problem, simply restate mutable args in each test to keep them independent.
To test properties of instances, or to test static methods and properties, you use exactly the same syntax you would for instance methods, 
 and Risei figures out the rest:
tests = [
    { on: VariegatedClass, with: [ ] },
    { of: "instanceMethod", for: "Returns true.", in: [ ], out: true },  // Typical use: instance method.
    { of: "staticMethod", for: "Returns false.", in: [ ], out: false },  // Static method: same syntax.  Empty .with needed.
    { of: "instanceProperty", for: "Returns 47.", in: [ ], out: 47 },    // Instance prop: same.  Empty .in needed.
    { of: "staticProperty", for: "Returns 98.", in: [ ], out: 98 }       // Static prop: same.  Empty .in needed.
];
.with and .in.   You can set them to empty arrays [] if needed.Although Risei figures out what it needs to in almost all cases, you can explicitly indicate member types if you want, and Risei then treats the code 
 as you specify:
{ of: "instanceMethod()", ... },                            // Trailing parens specify method.
{ of: ".someProperty", ... }, { of: "someProperty:", ... }  // Leading dot or trailing colon specify property.
{ of: "something", and: "static", ... },                    // You can add an .and of "static" for static memberss.
{ of: "something", and: "instance", ... },                  // You can add an .and of "instance" for instance members.
.of, .plus (covered here), and .from (covered here)..name.If you have static and instance members with the same names, you have to use an.andof"instance"to test the instance methods.
Apart from a very few built-in JavaScript methods liketoString(), this is a rare situation. Identical static and instance names are
the only case where specifying static / instance type in.andis actually necessary.
.plusTo make code dependencies return what your tests need, just spoof what you want using a .plus test property:
{ 
  on: TestedClass,
  ...
  plus: [ 
      { on: Dependency, of: "someMethod", as: 10 },  // Dependency.someMethod() returns 10 in this test.
      { of: "testedClassMethod", as: 11 }            // TestedClass.testedClassMethod() returns 11 in this test.
    ],
  ... 
}
There are many options available for spoofing, all laid out together in this example:
{ on: CombinerClass, 
  ...
  plus: [ 
    /* Mono-spoofing. */
    { on: ClassA, of: "someMethod", as: 10 },    // Spoof this ClassA method to return 10.
    { on: ClassB, of: "someMethod" },            // Spoof this ClassB method not to return (or do) anything.
    { of: "firstMethod", as: [ 7, 8, 9, 10 ] },  // Spoof a method on the tested class (CombinerClass).
    { of: "secondMethod" },                      // Spoof a method on the tested class not to do anything.
    { on: ClassC,                                // Spoof this ClassC method to be this nonce code using a runtime arg.
      of: "someMethod",
      as: (arg) => { return { color: arg }; } }, 
    /* Poly-spoofing. */
    { on: ClassD, as: [                          // Spoof two methods on ClassD at the same time.
        { of: "firstMethod", as: 11 }, 
        { of: "secondMethod", as: 12 } 
      ] },
    { as: [                                      // Spoof two methods on the tested class at the same time.
        { of: "firstMethod", as: 11 }, 
        { of: "secondMethod", as: 12 } 
      ] },
  ],
  ...
}
this of the tested instance.async and await when spoofing methods..and of "throws":You can test for a value thrown in code by adding an .and property of "throw" or "throws":
{ 
  on: TestedClass, of: "throwsWhenNoArgsGiven", and: "throws", 
  for: "When not given any args, throws an Error stating \"Argless.\".",
  in: [ ], out: new Error("Argless.") 
}
.and property is an extension point for special test conditions..and can contain any mix of valid .and values.undefined actuals with this.undef:You can test for an undefined value by setting .out to this.undef (or ATestSource.undefSymbol):
{
  on: TestedClass, of: "returnsArgGiven", 
  for: "When the arg is undefined, returns undefined.",
  in: [ undefined ], out: this.undef 
}
undefined itself directly for .out, because Risei treats it as a missing property..from:You can refine or replace the test's actual using the .from property, for instance for method side-effects or derivatives of the original actual.
.from:The .from can be the name of a property on the target class or its instance created for the test:
{
  on: TestedClass, of: "makeInstanceReady", 
  for: "When method is run, the .ready flag is set.", 
  in: [ ], out: true, 
  from: "ready"
}
.from:The .from can be a nonce function, which must always return a new actual value:
{
  on: TestedClass, of: "initDerivedObject", 
  for: "When method is run, output's .flag is cleared.",
  in: [ ], out: false,
  from: (test, actual) => { return actual.flag; }
}
actual parameter (found second) is the original actual value from running your targeted code.
test parameter (found first) is the assemblage of test properties you provided, plus others that Risei sets.   Here are the properties .from:
test.target — The instance created for the test, or the class for a static test.test.in — An array, most useful for methods that mutate their arguments.test.out — To replace the original expected value using test-time values.test.target, as this might cause problems..from to set test.actual, so even though you can set the latter in .from, it has no effect..from, in general it's better (and more flexible) to use .do and .undo for that.async and awaitsyntax..do.early, .do.late, and .undo:
If test state you need can't be created declaratively, you can set it up and then tear it down using .do and .undo.   The .do property has two stages, 
 .early and .late.
You set these properties to nonce functions:
{
  on: TestedClass, for: "When the file to read is not text, returns first ten bytes as hex.",
  do: { 
    early: () => { writeRandomTestFile(); },              // Steps before any test target instance is created.
    late: (test) => { test.out = readTestFileHex(10); }   // Steps after any instance is created, here altering a test property.
  },
  undo: () => { eraseRandomTestFile(); },                 // Steps after everything else in the test.
  in: [ ... ], out: 0x00,                                 // .out is replaced with varying data in .undo.
}
do.early and do.late if you only need one, which is normally the case.async and awaitsyntax.test parameter to alter the test itself if needed, though this is rare..and of "poly":You can call a method or property repeatedly in one test (poly-call it) by adding an .and property of "poly", with changes to what's in .in and .out:
{ 
  on: TestedClass, of: "sumSoFar", and: "poly", 
  for: "When called repeatedly, sums all args provided so far.",
  in: [ [ 1 ], [ 5 ], [ 8 ], [ 6 ], [ 4 ] ],  // An inner array for each call of the method (each empty if no args or property test).
  out: [ 1, 6, 14, 20, 24 ]                   // An array for the outputs of all calls, unless .from is used to customize output.
}
actual parameter / test.actual parameter in .from is an array of all the outputs..in and arrayed outputs in .out should be the same when testing all outputs.
.out, and use .from to return the value you want..and property can contain any mix of valid .and values.To test TypeScript code with Risei, you make sure the code is transpiled before the tests are run, and you point Risei to the transpiled .js files.
tsconfig.json:You set up the transpiler to output files to a separate directory with these two settings:
{
  "outDir": "dist/out-tsc",
  "noEmit": false
}
outDir, but it's best to avoid dist, which can interfere with frameworks' build steps.package.json:In the test script, add transpilation before the test run, and file deletion after it, each one conditional on the previous:
"scripts": {
  "test": "tsc && node ./node_modules/risei/index.js && rm -r ./dist/out-tsc"
}
tsc or an equivalent produces individually testable JavaScript files.outDir in tsconfig.json.You can run Risei manually with the same sequence of operations.
All import statements have to point to the JavaScript (.js) files in the outDir path or its subfolders:
import { TestedClass } from "../../dist/out-tsc/SubPath/TestedClass.js";
outDir folder itself (React).When problems cause test files not to load, you see a gold bar listing the syntax errors at fault:
 
Here are the most common problems:
| Error condition | Probable cause | 
|---|---|
| Gold bar text "Test loading failed for...  SyntaxError: Unexpected token ':'" | Missing comma in your tests in the named file, either between tests or between test props | 
| Gold bar text "Test loading failed for...  SyntaxError: Unexpected token '.'" | Stating the test list as this.tests = []instead oftests = []in the file named | 
| Other ... Unexpected token ...errors in gold bar | Some other syntax error in the file named, most likely a missing or extra delimiter | 
| Many test files failing all at once | Syntax error in the code targeted in those test files, or a dependency of it | 
If errors are thrown while testing, gold bars listing them appear at the bottom, and full stack traces appear amid the test output.   Errors that you 
 indicate you expect with an .and of "throw" or "throws" are not included.
There are two scopes for these throws: tested code, and framing code.   The former is the code you're testing.   Throws here can include anything 
 resulting from spoofing you define in .plus.
The latter is code used to test your code.   If you see a throw from framing code, most likely you have made a mistake in .do, .undo, or .from.
| Error condition | Probable cause | 
|---|---|
| Unexpected extra, failing test/s | An object intended only to change test properties lacks a .just/.only | 
| Unexpected actual values, or unexpected passes / fails in test runs | Test properties from previous tests not replaced or reset with []— or — Mutated args for tested code not restated in following tests to isolate them | 
| Tests written not appearing in runs | Not all properties needed to run tests stated before or in those tests | 
Test properties have longer names that you can use interchangeably with their short names.
| Short Name | Long Name | 
|---|---|
| on | type | 
| with | initors | 
| of | method | 
| for | nature | 
| in | inputs | 
| out | output | 
| plus | spoofed | 
| from | source | 
| and | factors | 
| do | enact | 
| undo | counteract | 
| just | only | 
.early and .late properties of .do / .enact.| Short Name | Long Name | 
|---|---|
| on | target | 
| of | method | 
| as | output | 
.from, using async and await normally..plus, .do, and .undo..and option of "instance".(threw).File objects now have a succinct custom display in outputs.constructor tests, constructed instances are now available in .from functions only as actual / test.actual.constructor tests, test.target is now the prototype of the tested class.async syntax is now fully supported, with no special test syntax required..from functions now take test and actual as parameters, rather than target and test.test.target..and of "poly" (AKA poly-calling)..name..do property and one-part .undo property..just.undefined in .out using this.undef / ATestSource.undefSymbol.Error objects are now compared accurately.., :, and () in .of, .plus, and .from, though it isn't necessary to do so.Error), rather than the Error's message text.ATestSource is now a default export, changing its imports from import { ATestSource } from to import ATestSource from.Date comparisons.
The oldest releases are no longer listed here, and old releases are dropped progressively over time.
Using the latest release is recommended.
These are the known minor issues:
toString() may not be recognized, nor any instance method that has .and value of "instance" for any methods like these that you test.The following are not supported at present:
export modules, AKA loose code.require().Proxy or ArrayBuffer in test assertions.Some of these may be supported in the future.
Risei is written by myself, Ed Fallin. I'm a longtime software developer who likes to find better ways to do things.
If you find Risei useful, consider spreading the word to other devs, making a donation, suggesting enhancements, or proposing sponsorships.
You can get in touch about Risei at riseimaker@gmail.com.
Risei is published for use under the terms of the MIT license:
Risei Copyright © 2023–2025 Ed Fallin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.