A handy little mocking library inspired by testdouble and @fluffy-spoon/substitute. It's main features are:
- easily creating nested mocks
- inferring types wherever possible
- make it possible to completely separate arrange/assert
testtriple completely focuses on mocking and leaves assertions to your test runner of choice.
npm install testtriple
import { mock, returns } from "testtriple";
type Human = {
name: string;
birthDate: Date;
getAge: () => number;
mother: Human;
father: Human;
};
const bob = mock<Human>({
name: "bob",
father: mock({
birthDate: new Date(1970, 1, 1),
mother: mock({
name: "helen",
getAge: returns(90),
}),
}),
});
console.log(bob.name); // "bob"
console.log(bob.father.birthDate); // Date(1970,1,1)
console.log(bob.father.mother.getAge()); // 90
You can use the mock<T>({...[subset of T]...})
function to create an object that only has some values set, but pretends to be a complete object. This can also be nested as deep as desired, whith all types being inferred.
import { mock } from "testtriple";
const bob = mock<Human>({
father: mock({
mother: mock({
name: "helen",
}),
}),
});
console.log(bob.father.mother.name); // "helen"
To mock function calls of an object created with mock({...})
you have to explicitly set the property to a function mock created using one of the functions below:
import { mock, returns } from "testtriple";
const bob = mock<Human>({
getAge: returns(10),
});
console.log(bob.getAge()); // 10
import { mock, throws } from "testtriple";
const eve = mock<Human>({
getAge: throws("You don't ask the age of a women!"),
});
console.log(eve.getAge()); // throws "You don't ask the age of a women!"
import { mock, resolves } from "testtriple";
const bob = mock<Human>({
getAge: resolves(10),
});
console.log(await bob.getAge()); // 10
import { mock, rejects } from "testtriple";
const eve = mock<Human>({
getAge: rejects("You don't ask the age of a women!"),
});
console.log(await eve.getAge()); // // throws "You don't ask the age of a women!"
This is the basic function that's used by all function mockers above. It takes one or more mimick functions. On every call of the original function, the next mimick function get's called and it's return value returned.
import { mock, spy } from "testtriple";
const bob = mock<Human>({
getAge: spy(
() => 10,
() => 20,
() => 30
),
});
console.log(bob.getAge()); // 10
console.log(bob.getAge()); // 20
console.log(bob.getAge()); // 30
It can also be used to chain returns
,throws
,resolves
and rejects
import { mock, spy, returns, resolves, throws } from "testtriple";
const bob = mock<Human>({
getAge: spy(returns(10), resolves(20), throws("he's dead, jim!")),
});
console.log(bob.getAge()); // 10
console.log(await bob.getAge()); // 20
console.log(bob.getAge()); // throws "he's dead, jim!"
testtriple doesn't do any assertions. But it gives you access to function calls and their parameters using callsOf
, callsOfAll
, callOrderOf
, and callDetailsOf
. You can then assert these calls using the test runner of your choice.
Used to verify the call order and arguments of a single function.
import { mock, spy, callsOf } from "testtriple";
const math = mock<Calulator>({
add: spy(),
multiply: spy(),
});
math.add(1, 2);
math.multiply(2, 2);
math.add(3, 8);
expect(callsOf(math.add)).toStrictEqual([
[1, 2],
[3, 8],
]);
Used to verify the call order and arguments across multiple functions.
import { mock, spy, callsOfAll } from "testtriple";
const math = mock<Calulator>({
add: spy(),
multiply: spy(),
});
math.add(1, 2);
math.multiply(2, 2);
math.add(3, 8);
expect(callsOfAll(math.add, math.multiply)).toStrictEqual([
[math.add, 1, 2],
[math.multiply, 2, 2],
[math.add, 3, 8],
]);
Used to verify the call order without arguments across multiple functions.
import { mock, spy, callOrderOf } from "testtriple";
const math = mock<Calulator>({
add: spy(),
multiply: spy(),
});
math.add(1, 2);
math.multiply(2, 2);
math.add(3, 8);
expect(callOrderOf(math.add, math.multiply)).toStrictEqual([
math.add,
math.multiply,
math.add,
]);
Used to simply verify the number of times a single function was called.
import { mock, spy, callDetailsOf } from "testtriple";
const math = mock<Calulator>({
add: spy(),
multiply: spy(),
});
math.add(1, 2);
math.multiply(2, 2);
math.add(3, 8);
const { called, callCount } = callDetailsOf(math.add);
expect(called).toBe(true);
expect(callCount).toBe(2);
While testtriple was inspired by the mentioned libraries, it does not mean that it focuses on the same core features. Instead, I tried to address some of weaknesses in those libraries. Specifically the creation of nested mocks in one go, without specifying the types for everything, and infer them instead. That's why the quick example features this functionality.
Doing the same in @fluffy-spoon/substitute
would look like this:
const bob = Substitute.for<Human>();
const father = Substitute.for<Human>();
const mother = Substitue.for<Human>();
mother.name.returns("helen");
mother.getAge().returns(90);
father.birthDate.returns(new Date(1970, 1, 1));
father.mother.returns(mother);
bob.father.returns(father);
While it's technically less lines of code, I find it incredibly hard to read. It gets even harder to read, when the type of sub-objects is not importable. Then it would look like this:
const bob = Substitute.for<Human>();
const father = Substitute.for<Human["father"]>();
const mother = Substitue.for<Human["father"]["mother"]>();
So, constructing such nested mocks is clearly the core feature of testtriple. Everything else is kept to a minimum, because other libraries already do it very well.
Assertions are a good example of this. Both testtouble and substitute provide some assertion functions, while testtriple does not. testtriple only gives you some functions to access the order and parameters of function calls and it's up to you how you assert that this data is correct. But for most cases, testtriple coupled with an assertion library should be able to do the job very well.
- Do you want to conveniently create complex, nested mocks with as readable code as possible? -> testtriple
- Do you want to have some more advanced functionality to verify function calls on mocks? -> testdouble/substitute
In every case you'll need some additional assertion library or assertion tools of your testrunner. With testtriple more that with the others.
I was lazy and just took the word double
from testdouble
, and made it triple
instead. But I'm sure you've already got that :D
MIT