@momsfriendlydevco/testa
Version:
Low-overhead, parallel-first testkit harness
301 lines (215 loc) • 10.3 kB
Markdown
@MomsFriendlyDevCo/Testa
========================
Low-overhead, parallel-first testkit harness with dependencies and a Mocha like UI updated for ESM.
```javascript
import test from '@momsfriendlydevco/testa';
test('simple test', ()=> {
test.expect(1).to.be.ok;
test.expect(1).to.be.a('number');
});
// Setup tests using a chainable syntax
test('auth').id('auth').do(()=>
fetch('https://acme.com/auth', {
method: 'POST',
headers: {
Authentication: `Bearer ${config.authToken}`,
},
})
);
// Apply dependencies
test('ping')
.depends('auth') // Wait for the above auth test to complete before we run this
.slow('30s') // Mark test as running slow if it takes >= 30 seconds
.timeout('1m') // ... and time it out at 1 minute
.do(async ()=> {
/* ... */
})
// Split multi-step processes up into stages
test('fetch entities', async t => {
t.stage('fetch users');
let users = await (await fetch('https://acme.com/api/users')).json();
t.log('there are', users.length, 'in the system'); // Output supplemental information during a test
t.stage('fetch projects');
let projects = await (await fetch('https://acme.com/api/projects')).json();
if (projects.length == 0)
t.warn('no projects in system'); // Warn about things without exiting
t.dump(users); // Dump complex information flows to temporary files to be examined later
if (projects.length == 0 && users.length == 0)
return t.skip('need at least 1 user + 1 project to run test'); // Skip out and say why
});
// Usual shortcut syntax applies
test('foo')
.skip('TODO: Not yet ready') // Don't actually run this, and optionally say why
.do(()=> /* ... */)
test('foo').only(()=> /* ... */) // Mark that only this test should run
test.before(()=> /* ... */) // Setup a test to run before everything else
test.after(()=> /* ... */) // Setup a test to run after everything else
test.priority(50).do(()=> /* ... */) // Or use priority levels (higher runs first)
```
CLI
---
Install into a project with `npm i @momsfriendlydevco/testa` or run as `npx @momsfriendlydevco/testa`.
```
Usage: testa [options] [files...]
Run testkits in parallel with dependencies
Options:
-l, --list List all queued tests and exit
-b, --bail Stop processing on the first error (implies
`--serial`)
-s, --serial Force run tests in serial (alias of `--limit
1`)
-p, --parallel <number> Set number of tests to run in parallel
(default: 5)
-g, --grep <expression> Add a grep expression filter for tests titles
+ IDs (can be specified multiple times)
(default: [])
-G, --invert-grep <expression> Add an inverted grep expression filter for
tests titles + IDs (can be specified multiple
times) (default: [])
-f, --fgrep <expression> Add a raw string expression filter for tests
titles + IDs (can be specified multiple
times) (default: [])
-F, --invert-fgrep <expression> Add an inverted raw string expression filter
for tests titles + IDs (can be specified
multiple times) (default: [])
--slow [timestring] Set the amount of time before a test is
considered slow to resolve. Can be any valid
timestring (default: "75ms")
--timeout [timestring] Set the amount of time before a test times
out. Can be any valid timestring (default:
"2s")
--ui [ui] Set the UI environment to use (default:
"bdd")
--debug Turn on various internal debugging output
-h, --help display help for command
```
Reasons
-------
**Another goddamned test library, dear god, why**
Yes it seems annoying that I'm adding to an existing well-trodden ground of testkits here but I was frustrated at some lacking features, namely:
1. No testkit seems to be able to do pre-dependencies correctly - what if one test requires another first? Its common to login or negotiate Auth credentials for some test units, why is the only way to do this screwing around with `before()` blocks or nesting tests?
2. No testkit I've seen puts parallelism first and foremost rather than an afterthought. This library is all about parallel with serial functionality as a secondary choice.
3. Context is outdated - arrow functions should be universal when declaring tests, no need to differentiate between `test(()=> {})` and `test(function() {})` contexts, just accept a universal context as an argument and work from there. This makes stuff like using `t.timeout()` or `t.skip()` much easier without having to care about a "strong" function context rather than arrow functions.
4. `beforeEach()` / `afterEach()` are anti-patterns and should not be supported - especially when we are doing things in parallel.
5. Why can't we say _why_ a test was skipped with `.skip()`?
6. `chai` / `expect()` should ship as standard - yes choice is nice but if thats what everyone uses anyway why bother adding another dependency + import header.
7. Tests should support sub-stages (see `TestaContext.stage()`) to clearly denote where in a long-running or complex test we are up to
8. Tests should be able to easily dump information for inspection without just spewing to the console (see `TestContext.dump()`)
API
===
test(title:String, handler:Function)
------------------------------------
The main test instanciator. Returns a `Testa` class instance.
test.expect()
-------------
Utility function which exposes a `chai#expect` function.
```javascript
import test from '@momsfriendlydevco/testa';
test('simple test', ()=> {
test.expect(1).to.be.ok;
test.expect(1).to.be.a('number');
});
```
Testa
-----
A Testa class instance.
Testa.id(id:String)
-------------------
Specify an ID for a test.
Returns the chainable instance.
Used to specify dependencies or refer to tests.
Testa.location(file:String, line:Number)
----------------------------------------
Indicate the location of the test.
This is automatically populated when using the `testa` bin.
Returns the chainable instance.
Testa.handler(handler:Function)
-------------------------------
Specify the test worker function.
Returns the chainable instance.
Testa.do(handler:Function)
--------------------------
Alias for `Testa.handler()`
Testa.title(title:String)
-------------------------
Specify a human readable title for a test. Used during logging.
Returns the chainable instance.
Testa.describe(description:String)
-------------------------
Add a more verbose description for a test.
Returns the chainable instance.
Testa.skip()
------------
Mark a test for skipping, these will not be run but marked as skipped when logging.
Returns the chainable instance.
Testa.only()
------------
Mark a test for 'only' inclusion. Unless overridden these will be the only tests run.
Returns the chainable instance.
Testa.priority(level:Number|String)
-----------------------------------
Set the priority order of a function.
Level can be a number (higher numbers run first) or a meta string such as 'BEFORE', 'AFTER'
Returns the chainable instance.
The `before()` and `after()` functions are really just aliases of `test.priority('BEFORE', ...)`
Testa.depends(...String)
------------------------
Set a pre-dependency for a test.
This marked test will not run less the dependency has run and successfully resolved first.
Returns the chainable instance.
Testa.before()
--------------
Alias for `test().priority('BEFORE', ...)`
Returns the chainable instance.
Testa.after()
-------------
Alias for `test().priority('AFTER', ...)`
Returns the chainable instance.
Testa.slow(timing:String|Number)
--------------------------------
Set the amount of time before a test is considered slow to resolve.
Can be a raw millisecond time or any valid Timestring.
Returns the chainable instance.
Testa.timeout(timing:String|Number)
-----------------------------------
Set the amount of time before a test should timeout.
Can be a raw millisecond time or any valid Timestring.
Returns the chainable instance.
Testa.run()
-----------
Actual test runner.
Creates a TestaContext, runs the handler function with that context and handles errors and general logging.
Testa.depends(...dependency:String)
-----------------------------------
Adds one or more IDs as a pre-dependency before running the test. These must resolve successfully before being able to continue.
Can be specified multiple times.
Returns the chainable instance.
TestaContext
------------
Context object passed as the functional context + the only argument to all test handler functions.
TestaContext.log(...msg:Any)
----------------------------
Log some test output.
Returns the chainable `TestaContext` instance.
TestaContext.warn(...msg:Any)
-----------------------------
Log some test output as a warning but don't exit the test.
Returns the chainable `TestaContext` instance.
TestaContext.dump(...msg:Any)
-----------------------------
Log some arbitrary output and continue the test.
This is designed mainly for large complex objects which may need to be dissected separately.
Returns the chainable `TestaContext` instance.
TestaContext.stage(...msg:Any)
------------------------------
Signal that we are at a specific sub-stage within a test function.
In most cases this acts as a bookmark.
Returns the chainable `TestaContext` instance.
TestaContext.skip(...msg:Any)
----------------------------
Notify that a test was skipped and indicate why.
Returns the chainable `TestaContext` instance.
TestaContext.wait(delay:Number|String)
--------------------------------------
Wrapper around `timestring()` + `setTimeout()` to wait for an arbitrary amount of time.
Returns a promise which will resolve when the delay has elapsed.