Install the package for your testing framework.
npm install --save-dev @given3/vitest
Test often move common setup and teardown into before/after hooks. Given3 allows these to be expressed in a clearly manner by wrapping them in a Given.
Without Given3:
import { describe, beforeEach, afterEach, it } from "vitest";
import { UserService } from "./user-service.js";
describe("User Service", () => {
// variable needs to be declared before beforeEach
let service: UserService;
const initialUsers = [{ id: 1, name: "John Doe" }];
beforeEach(() => {
service = new UserService();
service.setup(initialUsers);
});
// teardown for the service needs to be separate from the setup
afterEach(() => {
service.teardown();
});
it("should return a user", async () => {
await expect(service.getUser(1)).resolves.toEqual(initialUsers[0]);
});
});
With Given3:
import { given, cleanup } from "@given3/vitest";
import { describe, beforeEach, afterEach } from "vitest";
import { UserService } from "./user-service.js";
describe("User Service", () => {
const initialUsers = [{ id: 1, name: "John Doe" }];
// given holds but setup and teardown
const service = given(() => {
const s = new UserService();
s.setup(initialUsers);
cleanup(() => s.teardown());
return s;
});
it("should create a user", async () => {
// value of the service is lazily initialized when it's first used
await expect(service.value.getUser(1)).resolves.toEqual(initialUsers[0]);
});
});
In some cases the setup and teardown methods need to change, given3 gives you a way to do this without repeating any logic.
Without Given3:
import { describe, beforeEach, afterEach, it } from "vitest";
import { UserService } from "./user-service.js";
describe("User Service", () => {
let service: UserService;
const initialUsers = [{ id: 1, name: "John Doe" }];
beforeEach(() => {
service = new UserService();
service.setup(initialUsers);
});
afterEach(() => {
service.teardown();
});
it("should return a user", async () => {
await expect(service.getUser(1)).resolves.toEqual(initialUsers[0]);
});
describe("when there are no users", () => {
// teardown and re-initialize the service with the new user set
beforeEach(() => {
service.teardown();
service = new UserService();
service.setup([]);
});
it("should throw an error", async () => {
await expect(service.getUser(1)).rejects.toThrow();
});
});
});
With Given3:
import { given, cleanup } from "@given3/vitest";
import { describe, beforeEach, afterEach } from "vitest";
import { UserService } from "./user-service.js";
describe("User Service", () => {
const initialUsers = given(() => [{ id: 1, name: "John Doe" }]);
// givens can depend on each other,
// because they are lazily initialized the service will not be setup until
// it's first used.
const service = given(() => {
const s = new UserService();
s.setup(initialUsers.value);
cleanup(() => s.teardown());
return s;
});
it("should return a user", async () => {
await expect(service.value.getUser(1)).resolves.toEqual(
initialUsers.value[0],
);
});
describe("when there are no users", () => {
// define a new value of the initialUsers
initialUsers.define(() => []);
it("should throw an error", async () => {
await expect(service.value.getUser(1)).rejects.toThrow();
});
});
});
Given3's lazy evaluation allows you to capture common "act" and even some of the values you'd want to assert on in the setup of a test. This allows for single assertion tests to avoid repeated setup.
import { given, cleanup } from "@given3/vitest";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserService, User } from "./user-service.js";
describe("User Service", () => {
const initialUsers = given(() => []);
const analyticsService = given(() => vi.fn());
const userService = given(() => {
const s = new UserService();
s.setup(initialUsers.value, analyticsService.value);
cleanup(() => s.teardown());
return s;
});
const userQueryParam = given(() => 1);
// common "act" step
const userQueryResult = given(() =>
userService.value.getUser(userQueryParam.value),
);
describe("given a user exists", () => {
initialUsers.define(() => [{ id: userQueryParam.value, name: "John Doe" }]);
describe("when the user is requested", () => {
// make a query to the user service
beforeEach(() => userQueryResult.value);
it("should return the user", async () => {
// cached result won't requery the service
await expect(userQueryResult.value).resolves.toEqual(
initialUsers.value[0],
);
});
it("should call the analytics service", () => {
expect(analyticsService.value).toHaveBeenCalledWith({
action: "get",
userId: userQueryParam.value,
});
});
});
});
});
Given3's design allows you to refer to previous value of the given and "refine" them
import { given } from "@given3/vitest";
import { describe, it, expect } from 'vitest';
import { Factory } from "fishery";
import { faker } from "@faker-js/faker";
import { User, UserService } from "./user-service.js";
describe('User Service', () => {
const userFactory = given(() => Factory.define<User>(({ sequence }) => ({
id: sequence,
name: faker.person.fullName()
})));
// create 3 users, using whatever the user factory is at the time.
const initialUsers = given(() => userFactory.value.buildList(3));
const userService = given(() => {
const s = new UserService();
s.setup(initialUsers.value);
cleanup(() => s.teardown());
return s;
});
describe('given users have the admin role', () => {
// refine the user factory to produce users with the admin role
userFactory.define(() => userFactory.value.params({ roles: ['admin']}));
it('should mark returned user as admins', async () => {
const user = await userService.value.getUser(initialUsers.value[0].id);
expect(user.admin).toBeTruthy();
})
}
})
Given3 has 3 cache modes, specified in the cache
option passed as the second argument to define
:
'Each'
- the cache of previous values is cleared after each test'All'
- the cache of previous value is cleared after all the tests in a suitefalse
- values are not cached
When a cache is used Given3 tracks the other given's that are accessed and will invalidate the cache if any of them have their values updated.
A Cleanup function is passed to the definition as the first argument, or can also be imported from the given3 library. When a value is released from the cache any cleanup actions that where registered during it's initialization are called.
In addition to cleanup actions you can pass a Disposable
or AsyncDisposable
to the cleanup function in order to register it as a resource that needs to be disposed. Finally if your definition returns a Disposable
or AsyncDisposable
it will be disposed of automatically.
Given3 exports a createGivenConstructor function that takes a list of middleware. These intercept the creation of a given and allow changes to the object being returned.
@given3/core
exports a createGivenLibrary
function that can be used to implement Given3 for other testing frameworks.