Tutorials
Custom Test Matchers
nut.js relies on tests to avoid regressions and ensure that the framework works as expected. To do so, we use Jest as our test runner.
Jest provides a lot of built-in matchers to make assertions in your tests readable and easy to write.
test('two plus two is four', () => {
const expectedResult = 4;
expect(2 + 2).toBe(expectedResult);
});
In our codebase we make use of these built-in matchers because they are easy to use and well documented. But as time passed, we found ourselves repeating custom test helpers to assert certain conditions, especially so when writing end-to-end tests. This sparked the idea to take a closer look on how to write custom matchers for Jest.
What are Custom Jest Matchers?
Custom Jest matchers are user-defined matchers that extend Jest's default set. They allow developers to create specific test conditions that are not covered by the built-in matchers. This customization leads to more readable and maintainable tests, especially when dealing with complex data structures or specific business logic.
Advantages of Using Custom Matchers
Improved Readability: Custom matchers can make tests more readable and expressive. For example,
expect(user).toBeActive()
is more understandable than a series of checks on user properties.Reusable Logic: They promote DRY (Don't Repeat Yourself) principles by encapsulating repeated test logic into a single, reusable matcher.
Enhanced Testing Logic: Custom matchers can handle complex testing logic that built-in matchers cannot, like deep object comparisons or asynchronous validations.
Better Error Messages: They can provide clearer, more context-specific error messages, aiding in quicker debugging.
Implementing Custom Matchers in Jest
Creating a custom matcher in Jest involves extending the expect
object. Here's a basic structure:
expect.extend({
toBeDivisibleBy(received, argument) {
const pass = received % argument === 0;
if (pass) {
return {
message: () => `expected ${received} not to be divisible by ${argument}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be divisible by ${argument}`,
pass: false,
};
}
},
});
In this example, toBeDivisibleBy
is a custom matcher to check if a number is divisible by another number.
Custom nut.js Matchers
nut.js ships a set of custom matchers, available in the @nut-tree/nut-js
package.
import {jestMatchers} from "@nut-tree/nut-js";
To use them, you need to extend the expect
object in your test file.
expect.extend(jestMatchers);
This will make the custom matchers available in your tests. Currently, the following matchers are available:
toBeAt
toBeAt
is a matcher which works with the nut.js mouse instance and verifies mouse cursor position.
It receives a Point which specifies the expected mouse cursor position on screen.
const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");
expect.extend(jestMatchers);
describe("Basic test with custom toBeAt Jest matchers", () => {
it("should verify that cursor is at a certain position", async () => {
// GIVEN
const targetPoint = new Point(10, 10);
// WHEN
await mouse.setPosition(targetPoint);
// THEN
await expect(mouse).toBeAt(targetPoint);
});
});
It also supports negation as known from other matchers:
const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");
expect.extend(jestMatchers);
describe("Basic test with custom toBeAt Jest matchers", () => {
it("should verify that cursor is not at a certain position", async () => {
// GIVEN
const targetPoint = new Point(10, 10);
const wrongPoint = new Point(10, 10);
// WHEN
await mouse.setPosition(targetPoint);
// THEN
await expect(mouse).not.toBeAt(wrongPoint);
});
});
toBeIn
toBeIn
allows us to verify whether our mouse cursor is located within a certain Region or not.
const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");
expect.extend(jestMatchers);
describe("Basic test with custom toBeIn Jest matchers", () => {
it("should verify that cursor is within a certain region", async () => {
// GIVEN
const targetPoint = new Point(10, 10);
const targetRegion = new Region(5, 5, 10, 10);
// WHEN
await mouse.setPosition(targetPoint);
// THEN
await expect(mouse).toBeIn(targetRegion);
});
});
Just like toBeAt
, it supports negation:
const {jestMatchers, mouse, Point} = require("@nut-tree/nut-js");
expect.extend(jestMatchers);
describe("Basic test with custom toBeIn Jest matchers", () => {
it("should verify that cursor is not within a certain region", async () => {
// GIVEN
const targetPoint = new Point(10, 10);
const targetRegion = new Region(100, 100, 10, 10);
// WHEN
await mouse.setPosition(targetPoint);
// THEN
await expect(mouse).not.toBeIn(targetRegion);
});
});
toShow
The most interesting custom matcher is the toShow
one. It works with the screen instance of nut.js and thus is able to verify a whole lot of things...
It supports everything that is locatable using find, thus it is possible to write assertions for:
- images (requires an additional plugin, e.g. @nut-tree/nl-matcher
- windows (requires an additional plugin, e.g. @nut-tree/bolt
- text (requires an additional plugin, e.g. @nut-tree/plugin-ocr
- colors
Image Assertions
const {jestMatchers, screen, imageResource} = require("@nut-tree/nut-js");
require("@nut-tree/nl-matcher");
expect.extend(jestMatchers);
describe("Basic test with custom toShow Jest matchers", () => {
it("should verify that the screen shows a certain image", async () => {
// GIVEN
screen.config.resourceDirectory = "../../e2e/assets";
// WHEN
// THEN
await expect(screen).toShow(imageResource("an_image.png"));
});
});
Once again, it is also possible to negate an expectation:
const {jestMatchers, screen, imageResource} = require("@nut-tree/nut-js");
require("@nut-tree/nl-matcher");
expect.extend(jestMatchers);
describe("Basic test with custom toShow Jest matchers", () => {
it("should verify that the screen does not show a certain image", async () => {
// GIVEN
screen.config.resourceDirectory = "../../e2e/assets";
// WHEN
// THEN
await expect(screen).not.toShow(imageResource("different_image.png"));
});
});
Window Assertions
const {jestMatchers, screen, windowWithTitle} = require("@nut-tree/nut-js");
const {useBoltWindowFinder} = require("@nut-tree/bolt");
useBoltWindowFinder();
expect.extend(jestMatchers);
describe("Basic test with custom toShow Jest matchers", () => {
it("should verify that the screen shows a window with matching title", async () => {
// GIVEN
const windowTitleRegex = /.*custom-test-matchers.*/;
// WHEN
// THEN
await expect(screen).toShow(windowWithTitle(windowWithTitleRegex));
});
});
Text Assertions
const {jestMatchers, screen, singleWord} = require("@nut-tree/nut-js");
const {
LanguageModelType,
configure,
} = require("@nut-tree/plugin-ocr");
configure({languageModelType: LanguageModelType.BEST});
expect.extend(jestMatchers);
describe("Basic test with custom toShow Jest matchers", () => {
it("should verify that the screen shows a certain word", async () => {
// GIVEN
const wordToFind = "nut.js";
// WHEN
// THEN
await expect(screen).toShow(singleWord(wordToFind));
});
});
Color Assertions
const {jestMatchers, screen, RGBA, pixelWithColor} = require("@nut-tree/nut-js");
expect.extend(jestMatchers);
describe("Basic test with custom toShow Jest matchers", () => {
it("should verify that the screen shows a pixel with a certain color", async () => {
// GIVEN
const colorToFind = new RGBA(255, 0, 0, 255);
// WHEN
// THEN
await expect(screen).toShow(pixelWithColor(colorToFind));
});
});
What About Vitest?
Vitest is the next generation testing framework (as they like to call themselves) and it also supports custom matchers. And the cool thing is: It is compatible with Jest matchers!
So if you want to use Vitest, you can still use the custom matchers from nut.js.
import {expect, test} from "vitest";
import {screen, jestMatchers, windowWithTitle} from "@nut-tree/nut-js";
import {useBoltWindowFinder} from "@nut-tree/bolt";
useBoltWindowFinder();
expect.extend(jestMatchers);
test("toShow", async () => {
// GIVEN
const windowTitle = /.*SMBP2021.*/;
// WHEN
// THEN
await expect(screen).toShow(windowWithTitle(windowTitle));
});
Conclusion
Custom Jest matchers are a powerful tool to enhance the testing experience. They improve test readability, promote code reuse, handle complex testing scenarios, and provide better error messages. By creating custom matchers tailored to specific needs, nut.js ensures more efficient and effective testing.
All the best
Simon