Declarative JS tests with lazy evaluation for vitest.
Install vitest-plugin-set
using yarn
:
yarn add --dev vitest-plugins
yarn add --dev vitest-plugin-set
RSpec took the ruby world by storm with its declarative method of TDD. Since moving to JavaScript, I've wanted a similar way of declaring the setup for my tests. Here's what you would normally do to declare a test:
describe('User', () => {
let user;
describe('.update', () => {
beforeEach(() => {
user = new User({firstName: 'Mary', lastName: 'Lamb'});
});
describe('with valid firstName and lastName', () => {
let firstName;
let lastName;
beforeEach(() => {
firstName = 'Test';
lastName = 'User';
user.update({firstName, lastName});
});
it('should set firstName', () => {
expect(user.firstName).toEqual('Test');
});
it('should compute name', () => {
expect(user.name).toEqual('Test User');
});
});
describe('with invalid firstName', () => {
let firstName;
let lastName;
beforeEach(() => {
firstName = null;
lastName = null;
user.update({firstName, lastName});
});
it('should not override the original firstName', () => {
expect(user.firstName).toEqual('Mary');
});
});
});
});
Some notes:
- Because of scoping in javascript, we have to declare our variables outside the
beforeEach
blocks in order to reference them. - Our
beforeEach
blocks contain all of the setup code necessary which in this trivial example is at least 3 lines per test. - We can override variables in nested scopes, but following the chain is non-trivial because the actual variable declaration might be several layers up.
Here's what the same tests look like with using set
from vitest-plugin-set
:
describe('User', () => {
describe('.update', () => {
set('user', () => new User({firstName: 'Mary', lastName: 'Lamb'}));
describe('with valid firstName and lastName', () => {
set('firstName', () => 'Test');
set('lastName', () => 'User');
beforeEach(() => user.update({firstName, lastName}));
it('should set firstName', () => {
expect(user.firstName).toEqual('Test');
});
it('should compute name', () => {
expect(user.name).toEqual('Test User');
});
});
describe('with invalid firstName', () => {
set('firstName', () => null);
set('lastName', () => null);
beforeEach(() => user.update({firstName, lastName}));
it('should not override the original firstName', () => {
expect(user.firstName).toEqual('Mary');
});
});
});
});
Even in this trivial example, things are much easier to follow.
- We can declare
firstName
andlastName
as variables that we can then reference in ourbeforeEach
blocks. - We can break up the large
beforeEach
blocks into several distinctset
blocks. - We can easily set defaults in outer scopes (which may or may not be used within a particular test saving performance) and then overriding the values in nested blocks.
In JavaScript, let
is a keyword so the next closest word is...set
(which still keeps the meaning of what we're doing
- settings variables (lazily)).
If you want, you can import set
from vitest-plugin-set
at the top of every test:
import {set} from 'vitest-plugin-set';
If you want to install set
as a global, you can modify the test
section of your vitest.config.js
to include:
{
test: {
setupFiles: [
"vitest-plugin-set/setup",
]
}
}
Here's an example test that tests set
itself:
import {describe, it, expect} from 'vitest';
import {set} from 'vitest-plugin-set';
describe('set', () => {
set('a', () => 1);
set('b', () => 2);
set('c', () => 'hello world');
describe('variables set to primitives', () => {
it('should set a', () => {
expect(a).toEqual(1);
});
it('should set b', () => {
expect(b).toEqual(2);
});
it('should set c', () => {
expect(c).toEqual('hello world');
});
});
describe('variables set to arrays', () => {
set('a', () => [1, 2, 3]);
it('should properly set arrays', () => {
expect(a).toEqual([1, 2, 3]);
});
});
describe('variables set to objects', () => {
set('b', () => ({test: '1', value: 2, other: 'three'}));
it('should properly set objects', () => {
expect(b).toEqual({other: 'three', value: 2, test: '1'});
});
});
describe('nested set calls', () => {
set('a', () => 10);
it('should take the inner set', () => {
expect(a).toEqual(10);
});
});
describe('variables set within other set calls', () => {
set('b', () => a + 10);
it('should evaluate outer variables', () => {
expect(b).toEqual(11);
});
it('should be able to reference variables from the outer scope', () => {
expect(c).toEqual('hello world');
});
});
});