Alan Semenov Jest Mock XP

We recently held a Meetup in our office, where Alan Semenov, VP Engineering at Enonic held a talk about testing your Enonic apps with Jest and Mock XP.

Testing your Enonic apps with Jest and Mock XP

Previously we have received feedback hinting that our approach to testing JavaScript code might need improvement. We've heard you, and in response, we’re diving into the process of writing effective tests for your Enonic apps. Unconventionally, we'll start with a FAQ section to address common questions and set a solid foundation before delving into the details.

What Is Jest?

Jest is an open-source testing framework built with JavaScript for JavaScript. While it is primarily designed for JavaScript frameworks, it can also be used with other JavaScript-based languages, for example TypeScript. Jest is favoured for several reasons: 

  1. Ease of Setup: Jest requires no configuration in its minimal version, assuming you are writing vanilla JavaScript. Tests must be placed in specifically named folders and follow specific naming conventions, but beyond that, you can run tests with no additional configuration.
  2. Integration and Support: Jest was created by Facebook, the same team behind React. This native integration with React ensures long-term support. Additionally, Jest supports TypeScript, which is crucial for developers who prefer TypeScript.
  3. Performance: Jest is very fast as it runs tests in parallel.
  4. Built-in Features: Jest includes mocking out of the box, eliminating the need to install additional plugins for this functionality.
  5. Documentation: Jest is well-documented, providing comprehensive guides and examples.

Jest allows you to write tests for both client-side and server-side code. It supports integration with various frameworks, including React, Redux, Vue, Angular, Next.js, and Nuxt.js. The support extends beyond these, as there are tutorials, guides, and code examples available for many other frameworks.

Jest Setup

Setting up Jest is straightforward and flexible, whether you are using JavaScript or TypeScript. 

For JavaScript, you simply install Jest. It gets added to your `package.json`, and you're ready to go. 

For TypeScript, you use `ts-jest`, which includes both Jest and the necessary type definitions. It's recommended to use the wizard to generate the configuration file. The wizard will ask questions about your framework and test storage location, then generate the config file for you. If you prefer not to use the wizard, you can manually configure Jest by referring to the documentation for the relevant configuration properties.

Next, you add a script to your `package.json` for running tests, typically named "test." For frequent test runs, include the `--no-cache` option to ensure tests are always executed.

To integrate Jest with Gradle in an XP environment, add a Gradle task that links to the corresponding NPM script added in the previous step. This task runs the Jest tests every time the build process is triggered. If you remove the last line in this task, the tests will be skipped if there are no changes in either the test code or the source code being tested.

Finally, add a configuration to your Gradle setup to ensure that Jest tests are included whenever Gradle's test task is executed - that's the very last line in the screenshot abo. This ensures comprehensive testing by including Jest tests along with any other tests you might be running, such as Java tests or backend tests.

Syntax

The syntax of Jest is straightforward and involves three key methods: `describe`, `test`, and `expect`.

  • `describe` is used to group related tests.
  • `test` defines a specific use case you want to test.
  • `expect` is used to evaluate an expression and compare the result with a matcher.

Matchers are methods that allow you to assert various conditions. Common matchers include `toBe`, `toEqual`, `toBeTruthy`, `toBeFalsy`, `toBeNull`, and others. Matchers can also be inverted using `.not`. For example, `expect(value).not.toEqual(something)` checks if the value is not equal to a specific value.

Here is a simple example to illustrate the syntax:

```javascript
describe('Basic Math Operations', () => {
  test('adds 2 + 2 to equal 4', () => {
    expect(2 + 2).toBe(4);
  });

  test('object assignment', () => {
    const data = { one: 1 };
    data['two'] = 2;
    expect(data).toEqual({ one: 1, two: 2 });
  });

  test('true is truthy', () => {
    expect(true).toBeTruthy();
  });

  test('false is falsy', () => {
    expect(false).toBeFalsy();
  });

  test('null is null', () => {
    expect(null).toBeNull();
  });

  test('value is not 5', () => {
    expect(4).not.toBe(5);
  });
});
```

In this example:

  • The `describe` block groups tests related to basic math operations.
  • The `test` blocks define individual test cases.
  • The `expect` function evaluates expressions and compares them using matchers.

For comparing objects, use `toEqual` instead of `toBe`, as `toBe` is for primitive values. The matchers `toBeTruthy` and `toBeFalsy` are used to test boolean values.

This syntax makes it easy to write and understand tests, ensuring your code behaves as expected.

Simple Test

To start with a simple test, let's take an example of a Fibonacci sequence generator. We want to ensure it returns the correct sequence for a given number of elements. Here’s how you can test it in TypeScript using Jest:

  1. Import the required methods (`describe`, `expect`, `test`) from Jest.
  2. Import the Fibonacci sequence generator function.
  3. Write a test case within a `describe` block to verify the first ten numbers of the sequence.

```typescript
import { describe, expect, test } from '@jest/globals';
import { generateFibonacci } from './path-to-your-code';

describe('Fibonacci Sequence Generator', () => {
  test('generates the first ten Fibonacci numbers', () => {
    expect(generateFibonacci(10)).toEqual([0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
  });
});
```

This simple test checks that the function `generateFibonacci` correctly returns the first ten Fibonacci numbers.

Mocking

Next, let’s discuss mocking. Mocking is crucial for isolating the unit of code being tested, ensuring it doesn’t call external dependencies. This is different from spying, which only checks if a method was called without replacing it.

Here's an example of mocking a function:

  1. Imagine you have a TypeScript file that exports a `sanitize` method from a core library.
  2. When testing a function that depends on `sanitize`, you want to mock `sanitize` to control its behavior during the test.

Here's how you can mock the `sanitize` method using Jest:

  1. Import the necessary modules.
  2. Mock the library and the specific method of that library.
  3. Write your test case.

```typescript
import { describe, expect, test, jest } from '@jest/globals';
import { yourFunction } from './path-to-your-code';
import * as libCommon from 'libxpm-common';

jest.mock('libxpm-common', () => ({
  sanitize: jest.fn()
}));

describe('Your Function Tests', () => {
  test('uses the mocked sanitize method', () => {
    libCommon.sanitize.mockImplementation(() => 'mocked output');

    const result = yourFunction('input that requires sanitization');
   
    expect(result).toBe('expected result based on mocked sanitize');
    expect(libCommon.sanitize).toHaveBeenCalledWith('input that requires sanitization');
  });
});
```

In this example:

  • The `libxpm-common` library is mocked, and the `sanitize` method is replaced with a mock function.
  • The mock function's behavior is defined using `mockImplementation`.
  • The test verifies that `yourFunction` produces the expected result using the mocked `sanitize` method and checks that `sanitize` was called with the correct arguments.

By mocking dependencies, you can focus on testing your code in isolation without worrying about the actual implementation of external methods. This approach ensures your tests are reliable and do not depend on the behavior of external libraries.

Mock XP

Do you need to mock all of the Enonic XP API? The good news is, you don't have to do this manually. We’ve created an NPM module called Mock XP, which simplifies the process.

Mock XP is designed to help you run tests without needing the actual XP environment. This is especially useful for CI/CD pipelines on platforms like GitHub, where XP may not be running. Mock XP currently mocks nine core APIs and is available in version 1.0.

Here’s how to use mock XP:

  1. Installation: You install it (like any other NPM module) from `@enonic/mock-xp`.
  2. Usage: Import `Server` and create a mocked XP instance using `new Server()`.
  3. Usage: Mock one of the API libs currently mocked by `mock-xp` (for example, `LibNode` is a mock of Enonic XP's `lib-node` API).

Here’s a basic example:

```typescript
import { Server, LibNode } from '@enonic/mock-xp';

const mockServer = new Server();
const repo = mockServer.createRepo({id: 'test-repo'});
const nodeInstance = new LibNode({ repo });

// Now you can use methods from libnode, such as connect, create, etc.
nodeInstance.connect({...});
```

This setup allows you to use core XP methods without having XP running. It ensures your tests focus solely on your code.

Mock XP Example

Here's a more complex example to illustrate how mock XP can be used to create and verify nodes:

```typescript

import { Server, LibNode } from '@enonic/mock-xp';

const mockServer = new Server().createRepo({id: 'my-repo'});
const repo = mockServer.createRepo({id: 'test-repo'});
const nodeInstance = new LibNode({ repo });

const createdNode = nodeInstance.create({ _name: 'example-node' });
const expectedNode = { _name: 'example-node' };

expect(createdNode).toEqual(expectedNode);
```

In this example:

  • A mock server instance is created.
  • An instance of `LibNode` mock is created and connected to this mock server.
  • A node is created using the `createNode` method.
  • The created node is compared to the expected node to verify the test.

This way, you ensure that your tests are isolated from the actual XP environment, allowing you to focus on the functionality of your code without worrying about the underlying XP infrastructure. This is particularly useful for maintaining a robust testing suite that can run in various environments, including CI/CD pipelines.

Client-side (React)

When working with React for client-side testing, it's crucial to set up an environment that mimics the browser, including objects like `window` and `document`. You need to adjust your Jest configuration to use the appropriate environment, typically Jest DOM for client-side testing, as opposed to Node for server-side testing.

Start by installing React and the necessary packages. A simple React component, such as one that renders "Hello World," can serve as a basic example. The test setup involves importing the component and using React Test Renderer to simulate rendering the component without a browser. You can then use Jest to verify the rendered output, ensuring it contains the expected elements and text.

Best Practices

During our integration with Jest, we established several best practices:

  1. Separate Tests Folder: Keep tests in a different directory from the source code. This prevents TypeScript tests from being bundled with the production build, which can complicate the build process. We named our test directory 'Jest.'
  2. Folder Structure: Maintain separate folders for client-side and server-side tests. Mirror the source code structure within the tests folder to easily locate corresponding tests.
  3. Naming Conventions: Use a consistent naming pattern for test files, such as `.test.js` or `.spec.js`, to help Jest identify test files correctly.
  4. Simple, Single-Responsibility Tests: Avoid writing tests that cover multiple functionalities. Each test should be simple and focused on a single aspect to enhance maintainability and reduce the risk of breaking tests.

Additionally, we introduced a TypeScript Starter that simplifies the process of running Jest tests. This starter is a minimalist setup, excluding previous example code, allowing you to add your code and tests which will be automatically recognized and executed by Jest.

Running Tests without XP

In this demonstration, Alan showed how you can run tests without having Enonic XP running. First, we'll use the command `enonic create` to create a new example project. For this example, we named it "example Jest." The specific name isn't crucial here. A clean sandbox would be created and there's no need to start this sandbox since we're not running XP. 

To illustrate, Alan navigated to the project directory and showed us the structure. This is an actual XP app, not a mock setup. The source folder contains client and server tests, with a few client-side tests included.

Now, let's run the tests using Gradle. Since this is the first run, it will install some necessary NPM libraries. Once that's done, the NPM tests will execute, showing all tests passing without XP running. This is advantageous because it eliminates the need to start an XP instance, saving time. In this case, the tests took 1.5 seconds to complete. For context, in a larger project with over a thousand tests, it took around five seconds, demonstrating the efficiency of this approach.

Q&A

Q: Can you run NPM tests directly, not via gradle?
A: Yes, with `npm run test` command.

Q: Can you run tests with the CLI?
A: Yes, you can use the `enonic project test` command inside your XP project folder.

Regarding performance testing, there's integration with K6 for performance testing, traditionally used for this purpose. While performance testing has been a consideration for years, it hasn't been fully integrated yet, but it's on the list. You can still use K6 in your pipelines for now.

In summary, you can efficiently run tests without XP, leveraging tools like Gradle and NPM, and explore performance testing with K6 in your workflows.

Guide to Composable CMS

Related blog posts

Get some more insights 🤓


Get started with Enonic! 🚀