Skip to content

Testing

Testing is a fundamental part of software development, and Lime CRM customizations are no exception. This section will guide you through the different ways to set up and conduct tests on your custom components, ensuring they meet the desired requirements and function as expected.

In the context of Lime CRM customizations, testing often involves verifying the behavior of your web components, checking how they interact with services and the data state, and ensuring they respond correctly to different input and configurations.

By adopting a systematic testing approach, you can catch and address issues early, reduce the likelihood of bugs appearing in production, and maintain high-quality, reliable customizations. This guide aims to provide an overview of how to effectively test your Lime CRM customizations, focusing on unit tests, and explaining how to use testing tools and frameworks, such as Jest.

Remember that the goal of testing is not just to find bugs, but to produce a robust system that can be easily maintained and developed further. Good testing practices contribute to better software design and can significantly simplify the process of updating or adding new features to your customizations in the future.

Let's dive in and explore how you can leverage testing to create high-quality, reliable Lime CRM customizations.

Running Frontend Tests

When it comes to running tests for your frontend components, the process is straightforward. Ensure you are in the frontend folder of your customization. To execute all test files, use the command:

npm test

During active development, it's often helpful to watch for changes in your files and re-run the tests automatically. To do this, start a test watcher with the following command:

npm run test:watch

If you want to speed up the testing process, you can focus on a specific test file. Use a pattern that matches the name of the test file you want to run. For example, to only run tests within my-color-component.spec.tsx, use the command:

npm run test:watch -- color-component

This will ensure that only the tests in files matching color-component will run when you make changes.

Linting and Formatting TypeScript

Linting is essential in maintaining a clean, readable codebase. To lint your TypeScript code, execute the following command:

npm run lint

This will display any issues detected by the linter. Many of these issues can be fixed automatically. To do so, run:

npm run lint:fix

Note

Not all issues can be addressed automatically. Those that aren't auto-fixed will need your manual intervention.

Testing Pure Functions

While web components can have complex logic and state that make them challenging to test, pure functions offer a more straightforward testing route. The recommended approach is to keep web components as simple as possible and isolate complex logic into pure functions.

Pure functions should reside in their dedicated files and have their corresponding test files. These functions are easier to test because they always return the same result given the same input, and they don't have side effects. You can find more detailed guidance on how to write tests for pure functions in the Jest documentation.

Remember, the goal of testing is to ensure your customizations function as expected under various conditions and configurations, contributing to the overall reliability of your Lime CRM solution.

Testing Web Components

Web components can sometimes contain complex logic that requires dedicated testing. While the Stencil documentation provides an excellent overview of testing simple web components, components that utilize platform services or custom decorators from @limetech/lime-web-components require a more robust testing approach.

The @lundalogik/limeclient.js package contains all services in the webclient platform. It also exposes a /testing sub-package for creating a test client and test platform suited for your needs. All services function as usual, but HTTP requests require a registered handler for the endpoint. This configuration allows the test to run without depending on a live server.

In the following sections, we'll guide you through the process of setting up tests for a basic component.

The Component

The component we will be testing is straightforward. It renders a button, which, when clicked, sends an HTTP request. While waiting for the response, the component displays a spinner. Once the request is complete, the component emits an event. The component code would look something like this:

Example
my-component.tsx
import {
    LimeWebComponent,
    LimeWebComponentContext,
    LimeWebComponentPlatform,
    PlatformServiceName,
    SelectCurrentLimeType,
    SelectCurrentLimeObject,
    LimeType,
    LimeObject,
} from '@limetech/lime-web-components';
import { Component, h, Prop, State, Event, EventEmitter } from '@stencil/core';

@Component({
    tag: 'lwc-solution-test-my-component',
    shadow: true,
})
export class MyComponent implements LimeWebComponent {
    @Prop()
    public platform: LimeWebComponentPlatform;

    @Prop()
    public context: LimeWebComponentContext;

    @Event()
    public calculate: EventEmitter<string>;

    @State()
    private loading: boolean = false;

    @State()
    @SelectCurrentLimeType()
    private limetype: LimeType;

    @State()
    @SelectCurrentLimeObject()
    private company: LimeObject;

    public render() {
        if (this.loading) {
            return <limel-spinner />;
        }

        return (
            <limel-button
                label={`Calculate ${this.limetype.localname.singular}!`}
                onClick={this.handleClick}
            />
        );
    }

    private handleClick = async () => {
        this.loading = true;

        const result = await this.platform
            .get(PlatformServiceName.Http)
            .post('test/my-endpoint', {
                name: this.company?.name,
            });

        this.loading = false;

        this.calculate.emit(result.message);
    };
}

Defining Data Structures

We will be reusing the same data structure as for the Python tests. You can define the objects in the database by creating a similar YAML file:

odsl.yaml
company:
    lime:
        name: Lime Technologies AB
        rating: excellent

Test Setup

To set up the tests with a platform containing the limetypes and limeobjects defined in these files, add a setup.ts file to your project and reference it in the test section of the stencil.config.ts file.

Note

This setup must currently be done manually. Support for this setup may be added to lime-project at a later date.

stencil.config.ts
export const config: Config = {
    namespace: 'solution-test-lwc-components',
    testing: {
        setupFilesAfterEnv: ['./test/setup.ts'], // (1)!
    },
};
  1. setupFilesAfterEnv has been added to the testing section of the config, and the file test/setup.ts is going to run setup code before any tests are run.
test/setup.ts
import { LimeWebComponentPlatform } from '@limetech/lime-web-components';
import { TestClient } from '@lundalogik/limeclient.js/testing'; // (1)!
import { readFile } from 'fs/promises';
export let testPlatform: LimeWebComponentPlatform; // (2)!
export let testClient: TestClient;
let limetypesDsl: string;
let limeobjectsDsl: string;
beforeAll(async () => { // (3)!
    limetypesDsl = await readFile(__dirname + '/../../tests/dsl.yaml', {
        encoding: 'utf-8',
    });
    limeobjectsDsl = await readFile(__dirname + '/../../tests/odsl.yaml', {
        encoding: 'utf-8',
    });
});
beforeEach(async () => { 
    testClient = new TestClient({ // (4)!
        fixtures: {
            limetypes: limetypesDsl,
            limeobjects: limeobjectsDsl,
        },
    });
    const platform = testClient.createPlatform('LimeCRMWebClient');
    await platform.load(); // (5)!
    testPlatform = platform as LimeWebComponentPlatform; // (6)!
});
  1. The TestClient can be imported from @lundalogik/limeclient.js/testing, it will be used to create a client and platform suitable for testing.
  2. testPlatform and testClient are exported so that they can be used in each test file.
  3. beforeAll will run once before all tests. Reading the data structure files only needs to be done once since they never change between tests.
  4. Before each test is run, a new TestClient is created with the data structure from the files.
  5. platform.load() will load all platform services, such as setting up limetypes etc.
  6. Set the exported testPlatform to the newly created platform.

Before using @lundalogik/limeclient.js/testing, ensure you've installed the @lundalogik/limeclient.js package:

npm install -D @lundalogik/limeclient.js

Testing the Component

With the setup complete, we can begin writing basic tests for the component. The test setup for the component will create a test page, render the component on the page, and ensure that a limeobject has been loaded:

src/components/my-component/my-component.spec.tsx
import { testPlatform, testClient } from '../../../test/setup'; // (1)!
import { SpecPage, newSpecPage } from '@stencil/core/testing';
import { MyComponent } from './lwc-solution-cool-solution-foo';
import { h } from '@stencil/core';
import { PlatformServiceName } from '@limetech/lime-web-components';
let page: SpecPage;
let handler: jest.Mock; 
let button: HTMLElement; 
beforeEach(async () => {
    testPlatform
        .get(PlatformServiceName.LimeObjectRepository)
        .loadObject('company', 1001); // (2)!
    handler = jest.fn(); // (3)!
    page = await newSpecPage({
        components: [MyComponent],
        template: () => ( // (4)!
            <lwc-solution-test-my-component
                platform={testPlatform}
                context={{
                    limetype: 'company',
                    id: 1001,
                }}
                onCalculate={handler}
            />
        ),
    });
    await page.waitForChanges(); // (5)!
    button = page.root.shadowRoot.querySelector('limel-button'); // (6)!
});
  1. The testPlatform and testClient that were created in the global setup are imported so that they can be used in the test.
  2. No objects are loaded into the state of the client by default. By using the LimeObjectRepository service on the platform, limeobjects can be loaded just as they would in a real scenario.
  3. A handler is created in order to test the emitted event from the component
  4. Render the component on the test page, give it the test platform and create a context for the component.
  5. Wait for the page to render
  6. Get a reference to the rendered button

Test Rendering

The first basic test just ensures that the component renders correctly.

test('the component is rendering', () => {
    expect(page.root).toEqualHtml(`
        <lwc-solution-test-my-component>
            <mock:shadow-root>
                <limel-button label="Calculate company!"></limel-button>
            </mock:shadow-root>
        </lwc-solution-test-my-component>
    `);
});

Test the Emitted Event

The second test will click the button which will send a request to the server.Since no server exists in the test environment, a fake endpoint handler can be added to the client. The component will then emit an event with the result of the request.

test('clicking the button emits an event', async () => {
    testClient.fetcher.addServerHandler( // (1)!
        'POST',
        '/dev-app/test/my-endpoint',
        endpoint
    );
    button.click();
    await page.waitForChanges(); // (2)!
    expect(handler).toBeCalledTimes(1);
    expect(handler).toBeCalledWith(
        expect.objectContaining({
            detail: 'Lime Technologies AB is OK!', // (3)!
        })
    );
});
function endpoint(request) { // (4)!
    return {
        message: `${request.body.name} is OK!`,
    };
}
  1. Add a handler for the request sent to the server. The application name used for the test client is dev-app
  2. After the button is clicked, wait for the page to update
  3. Ensure that the handler has been called with the correct data
  4. The test endpoint

Test Spinner Rendering

Tests should always run as fast as possiblem but in this case this is a problem since there is no chance for the spinner to render before the endpoint returns its value. To fix this, the endpoint can be slowed down a bit to ensure that the spinner has a chance to render.

test('clicking the button renders a spinner instead', async () => {
    testClient.fetcher.addServerHandler( 
        'POST',
        '/dev-app/test/my-endpoint',
        slowEndpoint // (1)!
    );
    button.click();
    await page.waitForChanges(); // (2)!
    expect(page.root).toEqualHtml(`
        <lwc-solution-test-my-component>
            <mock:shadow-root>
                <limel-spinner></limel-spinner> 
            </mock:shadow-root>
        </lwc-solution-test-my-component>
    `); // (3)!
});
async function slowEndpoint(request) {
    await new Promise(resolve => setTimeout(resolve)); // (4)!
    return endpoint(request);
}
  1. Add the server handler again, but use a slower endpoint so there is a chance for the spinner to render
  2. Wait for the page to update and render
  3. Assert that the spinner is now rendering instead
  4. This ensures that the endpoint will wait for 0ms before continuing. Even 0ms is enough to let the test continue before the endpoint continues its work.

All the Test

Below is the code for the test file in full

Example
my-component.spec.ts
import { testPlatform, testClient } from '../../../test/setup';
import { SpecPage, newSpecPage } from '@stencil/core/testing';
import { MyComponent } from './lwc-solution-cool-solution-foo';
import { h } from '@stencil/core';
import { PlatformServiceName } from '@limetech/lime-web-components';

let page: SpecPage;
let handler: jest.Mock;
let button: HTMLElement;

beforeEach(async () => {
    testPlatform
        .get(PlatformServiceName.LimeObjectRepository)
        .loadObject('company', 1001);

    handler = jest.fn();
    page = await newSpecPage({
        components: [MyComponent],
        template: () => (
            <lwc-solution-test-my-component
                platform={testPlatform}
                context={{
                    limetype: 'company',
                    id: 1001,
                }}
                onCalculate={handler}
            />
        ),
    });

    await page.waitForChanges();
    button = page.root.shadowRoot.querySelector('limel-button');
});

test('the component is rendering', () => {
    expect(page.root).toEqualHtml(`
        <lwc-solution-test-my-component>
            <mock:shadow-root>
                <limel-button label="Calculate company!"></limel-button>
            </mock:shadow-root>
        </lwc-solution-test-my-component>
    `);
});

test('clicking the button emits an event', async () => {
    testClient.fetcher.addServerHandler(
        'POST',
        '/dev-app/test/my-endpoint',
        endpoint
    );

    button.click();
    await page.waitForChanges();

    expect(handler).toBeCalledTimes(1);
    expect(handler).toBeCalledWith(
        expect.objectContaining({
            detail: 'Lime Technologies AB is OK!',
        })
    );
});

test('clicking the button renders a spinner instead', async () => {
    testClient.fetcher.addServerHandler(
        'POST',
        '/dev-app/test/my-endpoint',
        slowEndpoint
    );

    button.click();
    await page.waitForChanges();

    expect(page.root).toEqualHtml(`
        <lwc-solution-test-my-component>
            <mock:shadow-root>
                <limel-spinner></limel-spinner>
            </mock:shadow-root>
        </lwc-solution-test-my-component>
    `);
});

function endpoint(request) {
    return {
        message: `${request.body.name} is OK!`,
    };
}

async function slowEndpoint(request) {
    await new Promise(resolve => setTimeout(resolve));

    return endpoint(request);
}

The testing process is a crucial step in ensuring the smooth operation of your customization. A well-tested customization means a reliable Lime CRM solution.