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:
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:
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:
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:
This will display any issues detected by the linter. Many of these issues can be fixed automatically. To do so, run:
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
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:
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.
export const config: Config = {
namespace: 'solution-test-lwc-components',
testing: {
setupFilesAfterEnv: ['./test/setup.ts'], // (1)!
},
};
setupFilesAfterEnv
has been added to thetesting
section of the config, and the filetest/setup.ts
is going to run setup code before any tests are run.
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)!
});
- The
TestClient
can be imported from@lundalogik/limeclient.js/testing
, it will be used to create a client and platform suitable for testing. testPlatform
andtestClient
are exported so that they can be used in each test file.beforeAll
will run once before all tests. Reading the data structure files only needs to be done once since they never change between tests.- Before each test is run, a new
TestClient
is created with the data structure from the files. platform.load()
will load all platform services, such as setting up limetypes etc.- Set the exported
testPlatform
to the newly created platform.
Before using @lundalogik/limeclient.js/testing
, ensure you've installed the @lundalogik/limeclient.js
package:
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:
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)!
});
- The
testPlatform
andtestClient
that were created in the global setup are imported so that they can be used in the test. - 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. - A handler is created in order to test the emitted event from the component
- Render the component on the test page, give it the test platform and create a context for the component.
- Wait for the page to render
- 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!`,
};
}
- Add a handler for the request sent to the server. The application name used
for the test client is
dev-app
- After the button is clicked, wait for the page to update
- Ensure that the handler has been called with the correct data
- 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);
}
- Add the server handler again, but use a slower endpoint so there is a chance for the spinner to render
- Wait for the page to update and render
- Assert that the spinner is now rendering instead
- 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
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.