This is the main module. All uvu
tests require that either suite
or test
(or both) be imported.
You may declare multiple Suites
in the same file. This helps with organization as it group test output in a more readable fashion and allows related items to remain neighbors.
You should choose uvu.suite
if/when you'd like to leverage the additional organization.
You should choose uvu.test
if you don't care about organization and/or are only planning on testing a single entity.
There is no penalty for choosing uvu.suite
vs uvu.test
. In fact, uvu.test
is an unnamed Suite!
No matter which you choose, the Suite's run
must be called in order for it to be added to uvu
's queue.
Note: Because of this API decision,
uvu
test files can be executed withnode
directly!
Returns: Suite
Creates a new Suite
instance.
Of course, you may have multiple Suite
s in the same file.
However, you must remember to call run()
on each suite!
Type: String
The name of your suite.
This groups all console output together and will prefix the name of any failing test.
Type: any
Default: {}
The suite's initial context value, if any.
This will be passed to every hook and to every test block within the suite.
Note: Before v0.4.0,
uvu
attempted to provide read-only access within test handlers. Ever since,context
is writable/mutable anywhere it's accessed.
Returns: void
If you don't want to separate your tests into groups (aka, "suites") – or if you don't plan on testing more than one thing in a file – then you may want to import test
for simplicity sake (naming is hard).
Important: The
test
export is just an unnamedSuite
instance!
Type: String
The name of your test.
Choose a descriptive name as it identifies failing tests.
Type: Function<any>
or Promise<any>
The callback that contains your test code.
Your callback may be asynchronous and may return
any value, although returned values are discarded completely and have no effect.
All uvu
test suites share the same API and can be used in the same way.
In fact, uvu.test
is actually the result of unnamed uvu.suite
call!
The only difference between them is how their results are grouped and displayed in your terminal window.
API
Every suite instance is callable.
This is the standard usage.
For this suite
, only run this test.
This is a shortcut for isolating one (or more) test blocks.
Note: You can invoke
only
on multiple tests!
Skip this test block entirely.
Invoke the provided callback
before this suite begins.
This is ideal for creating fixtures or setting up an environment.
Please see Hooks for more information.
Invoke the provided callback
after this suite finishes.
This is ideal for fixture or environment cleanup.
Please see Hooks for more information.
Invoke the provided callback
before each test of this suite begins.
Please see Hooks for more information.
Invoke the provided callback
after each test of this suite finishes.
Please see Hooks for more information.
Start/Add the suite to the uvu
test queue.
Important: You must call this method in order for your suite to be run!
Example
Check out
/examples
for a list of working demos!
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import * as dates from '../src/dates';
const Now = suite('Date.now()');
let _Date;
Now.before(() => {
let count = 0;
_Date = global.Date;
global.Date = { now: () => 100 + count++ };
});
Now.after(() => {
global.Date = _Date;
});
// this is not run (skip)
Now.skip('should be a function', () => {
assert.type(Date.now, 'function');
});
// this is not run (only)
Now('should return a number', () => {
assert.type(Date.now(), 'number');
});
// this is run (only)
Now.only('should progress with time', () => {
assert.is(Date.now(), 100);
assert.is(Date.now(), 101);
assert.is(Date.now(), 102);
});
Now.run();
Your suite can implement "hooks" that run before and/or after the entire suite, as well as before and/or after the suite's individual tests.
It may be useful to use suite.before
and suite.after
to set up and teardown suite-level assumptions like:
- environment variables
- database clients and/or seed data
- generating fixtures
- mocks, spies, etc
It may be appropriate to use suite.before.each
and suite.after.each
to reset parts of suite's context, or for passing values between tests. This may include — but of course, is not limited to — rolling back database transactions, restoring a mocked function, etc.
Important: Any
after
andafter.each
hooks will always be invoked – including after failed assertions.
Additionally, as of [email protected]
, hooks receive the suite's context
value. They are permitted to modify the context
value directly, allowing you to organize and abstract hooks into reusable setup/teardown blocks. Please read Context for examples and more information.
Example: Lifecycle
The following implements all available hooks so that their call patterns can be recorded:
test.before(() => {
console.log('SETUP');
});
test.after(() => {
console.log('CLEANUP');
});
test.before.each(() => {
console.log('>> BEFORE');
});
test.after.each(() => {
console.log('>> AFTER');
});
// ---
test('foo', () => {
console.log('>>>> TEST: FOO');
});
test('bar', () => {
console.log('>>>> TEST: BAR');
});
test.run();
// SETUP
// >> BEFORE
// >>>> TEST: FOO
// >> AFTER
// >> BEFORE
// >>>> TEST: BAR
// >> AFTER
// CLEANUP
When using suite hooks to establish and reset environments, there's often some side-effect that you wish to make accessible to your tests. For example, this may be a HTTP client, a database table record, a JSDOM instance, etc.
Typically, these side-effects would have to be saved into top-level variables that so that all parties involved can access them. The following is an example of this pattern:
const User = suite('User');
let client, user;
User.before(async () => {
client = await DB.connect();
});
User.before.each(async () => {
user = await client.insert('insert into users ... returning *');
});
User.after.each(async () => {
await client.destroy(`delete from users where id = ${user.id}`);
user = undefined;
});
User.after(async () => {
client = await client.end();
});
User('should not have Teams initially', async () => {
const teams = await client.select(`
select id from users_teams
where user_id = ${user.id};
`);
assert.is(teams.length, 0);
});
// ...
User.run();
While this certainly works, it can quickly become unruly once multiple suites exist within the same file. Additionally, it requires that all our suite hooks (User.before
, User.before.each
, etc) are defined within this file so that they may have access to the user
and client
variables that they're modifying.
Instead, we can improve this by writing into the suite's "context" directly!
Note: If it helps your mental model, "context" can be interchanged with "state" – except that it's intended to umbrella the tests with a certain environment.
const User = suite('User');
User.before(async context => {
context.client = await DB.connect();
});
User.before.each(async context => {
context.user = await context.client.insert('insert into users ... returning *');
});
User.after.each(async context => {
await context.client.destroy(`delete from users where id = ${user.id}`);
context.user = undefined;
});
User.after(async context => {
context.client = await context.client.end();
});
// <insert tests>
User.run();
A "context" is unique to each suite and can be defined through suite()
initialization and/or modified by the suite's hooks. Because of this, hooks can be abstracted into separate files and then attached safely to different suites:
import * as $ from './helpers';
const User = suite('User');
// Reuse generic/shared helpers
// ---
User.before($.DB.connect);
User.after($.DB.destroy);
// Keep User-specific helpers in this file
// ---
User.before.each(async context => {
context.user = await context.client.insert('insert into users ... returning *');
});
User.after.each(async context => {
await context.client.destroy(`delete from users where id = ${user.id}`);
context.user = undefined;
});
// <insert tests>
User.run()
Individual tests will also receive the context
value. This is how tests can access the HTTP client or database fixture you've set up, for example.
Here's an example User
test, now acessing its user
and client
values from context instead of globally-scoped variables:
User('should not have Teams initially', async context => {
const { client, user } = context;
const teams = await client.select(`
select id from users_teams
where user_id = ${user.id};
`);
assert.is(teams.length, 0);
});
TypeScript
Finally, TypeScript users can easily define their suites' contexts on a suite-by-suite basis.
Let's revisit our initial example, now using context and TypeScript interfaces:
interface Context {
client?: DB.Client;
user?: IUser;
}
const User = suite<Context>('User', {
client: undefined,
user: undefined,
});
// Our `context` is type-checked
// ---
User.before(async context => {
context.client = await DB.connect();
});
User.after(async context => {
context.client = await context.client.end();
});
User.before.each(async context => {
context.user = await context.client.insert('insert into users ... returning *');
});
User.after.each(async context => {
await context.client.destroy(`delete from users where id = ${user.id}`);
context.user = undefined;
});
// Our `context` is *still* type-checked 🎉
User('should not have Teams initially', async context => {
const { client, user } = context;
const teams = await client.select(`
select id from users_teams
where user_id = ${user.id};
`);
assert.is(teams.length, 0);
});
User.run();