Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...

...


Warning
titleOut of date

This information may be out of date. Practices for erm apps specifically have shifted from the stripes standard,  using instead a centralised library sitting between stripes-testing and our modules stripes-erm-testing. The approach to mocking etc has also shifted, and should now be more in line with the jest practices outlined in their documentation.

Table of Contents

This document is a starting/reference guide for writing unit tests for the UI modules within ERM. 

RTL (React testing library) is a testing utility for React that has been adopted by the FOLIO community as the framework for writing Unit tests within the UI modules. 

...

Jest is the test runner which react recommends and works alongside RTL. Jest lets you access the dom and make assertions against it within your tests.

RTL/JEST setup within ERM:

    1. The setup for the RTL/jest infrastructure lies within the stripes-erm-components under the test/jest folder. (PR that set it up for reference)
    2. We have chosen stripes-erm-components to place this infrastructure because all the ERM UI modules have a dependency on stripes-erm-components which allows us to place the test infrastructure in just one module and pull it directly into the individual ui-* modules with a failry simple setup ( ui-agreements). The setup is pulled from this config file in erm-components
    3.  The __mock__ folder mocks the entire underlying stripes libraries, for eg: stripes-core, stripes-smart-components, stripesConfig, stripes-connect etc. These mocks are what let us not worry about any stripes dependencies we may have while testing, and just be concerned with testing the individul UI components in isolation.

Note: Its very important all the developers understand what each of the mock is actually mocking and also understand the jest configuration within the jest.config.js file. Understanding these details will help debug the tests quickly specially when the errors/issues within the tests are caused at the stripes level.

Writing RTL tests:

Before writing the RTL tests, its important to arrange where you would want to place the test file. RTL picks up test files with extension __filename__.test.jsStructurally its easier to group the test file alongside the actual component thats being tested. Refer to the AddToBasketButton folder that has the component and its corresponding test placed in the same folder. 

...

    • Importing React
    • Importing the stripes mocks from erm-components
    • Importing the TestForm (if your component is rendered within a Form), renderWithIntl (function that renders the component to the dom with necessary translations)
    • userEvent thats used to trigger events such as clicks, hover etc
    • Finaly import the component that is being tested


Mock:

Apart from the stripes/intl mocks that were imported in the previous step from erm-components, this section includes mocks such as mocking an API request, or mocking  a callback (spy). We use jests mocking API for this purpose.

...

Note: You can clear the mocks using the mockClear() helper from jest. This is useful when you want to mock the function only for a specific
test and want to revert to the default implementation before the next step.

Arrange: 

The next step is to render the component using either the render helper or renderWithIntl (which is just a wrapper around the render helper but also includes translations).

...

Note: translationProperties returns list of translations that needs to be passed in to the renderWithIntl helper.
If we do not pass it, then a translation entry such as 'ui-agreements.agreement: agreement' would be rendered as
'ui-agreements.agreement' instead of 'agreement' in the dom.

Act:

This step is where we can trigger user actions such as clicks, hover etc.

...

To trigger a button with a label 'Submit', you could use the userEvent.click api:

Assert:

Final step is to assert if your test is behaving as expected.

The following assertion expects the handleSubmit callback to be called once when the submit button is clicked.


Queries

...

Before moving on to an example, queries are an important ascpect aspect of testing and choosing the right query within your tests is crucial.

Queries are the methods that RTL gives you to find elements on the page. There are a number of ways in which we can grab an element on a page. 

The following table describes the selectors that you need to use as a priority.

Image Modified

Priority should be give according to the priorities listed here.

...

           To grab a button by the label Save & close, you can either use:

     1) getByRole('button', { name: 'Save & close' })
2) getBylabelText('Save & close')
     Note: You might be tempted to pass data-testid to an element and find an element using the getByTestId query. This attribute is 
provided by RTL only as an escape hatch and should be avoided wherever possible. The attribute is going to eventualy end up
on the dom and we do not want to pollute the dom with avoidable attributes.

Putting all the above steps together:

import React from 'react';
import '@folio/stripes-erm-components/test/jest/__mock__';
import { TestForm, renderWithIntl } from '@folio/stripes-erm-components/test/jest/helpers';
import userEvent from '@testing-library/user-event';
import BasketSelector from './BasketSelector';
const onAdd = jest.fn();
const onSubmit = jest.fn();

const basketSelectorProps = {
'addButtonLabel':'add button',
'autoFocus':true,
'basket':[{
'id':'5650325f-52ef-4ac3-a77a-893911ccb666',
'class':'org.olf.kb.Pkg',
'name':'Edward Elgar:Edward Elgar E-Book Archive in Business & Management, Economics and Finance:Nationallizenz',
'suppressFromDiscovery':false,
'label': 'basketSelector'
}]
};

describe('BasketSelector', () => {
test('renders add To Basket button', () => {
const { getByText } = renderWithIntl(
<TestForm onSubmit={onSubmit}>
<BasketSelector {...basketSelectorProps}/>
</TestForm>
);

   expect(getByLabelText(/basketSelector/i)).toBeInTheDocument();
userEvent.click(getByText('add button'));
expect(onAdd.mock.calls.length).toBe(1);
});
});

Running the tests locally

...

To run the test in your local workspace:

...

      yarn test:jest BasketSelector;

Running Coverage report

...

Once you run the tests locally you should have an artifacts folder generated locally in your app folder.

...

Idealy we would like to have 100% coverage on all the components. Sometimes it feels too trivial to cover certain cases, i.e. the effort in writing the test outweighs the condition you are checking for and that decision is left to the developer.

Using Interactors

...

One of the advantages of having a custom design system and component library within FOLIO is that we don't start from scratch when writing tests that target an application using it. We know very precisely what the structure the DOM will take, and we can use that knowledge to our advantage when it comes to writing tests, both for manipulating the UI and also for making assertions against it.

Interactors provide us an abstraction between the HTML of our tests, so that not only are tests easy to write, but as the underlying components change and evolve, the tests do not need to change. 

Refer the documentation for interactors to find the list of all the interactors currently available. A good way to understand how tests need to be structured/written using the interactors is to differentiate between the traditional RTL approach and the interactors way. Imagine we are writing a test to assert that a button (rendered via the stripes component Button component) with a specific label is rendered.

...

The EresourceSections have a good set of tests that currently use the MCL, KeyValue, Button and TextField interactors. The documentation for the interactors as pointed above list all the different types of filters, actions and locators that the interactor accepts.

...

, Button and TextField interactors. The documentation for the interactors as pointed above list all the different types of filters, actions and locators that the interactor accepts.

Writing tests for Route level components:

The top level Route components within ERM apps serves as a single component for fetching all the data at the top level required by all its nested children and defining the callback handlers to be passed to the child components. This makes testing these Route level components slightly tricky. The reason being  Route component acts more like a container but not strictly presentation which makes it difficult to write UI tests as there is no DOM to work against and test out the long list of callback handlers each of them contains. Sure, we can let the child components render as is without being mocked but we dont do that as an overall practise within ERM apps and let the tests be as 'unit'y as possible instead of breaking into the integration tests realm.

Testing the route component just to check if the mocked child component has been rendered to the dom as expected is one test scenario but we need lot more to actually test the handlers defined within the Route. To tackle this problem, we mock the child component in a way that also render mock Buttons with our custom callback handlers and we click them via our tests to trigger the callbacks and verify if they are doing the right thing.


Example scenario: Testing the AgreementEditRoute which renders an AgreementForm:


          The AgreementForm component receives the following handlers onBasketLinesAdded, OnClose, etc passed in from the Route component. To make sure that these callbacks are invoked me create mock Buttons and pass these callbacks which gets triggered on an OnClick of those Buttons.

          eg: 


           Image Added

            To render this Button to the dom, our AgreementForm mock needs to be like so:

            Image Added


To test the callback has been invoked when the Button is clicked:

Image Added

queryUpdateMock (a mock that mocks the query.update() function call within the Route) in this case is what gets invoked when the callback handler for the onBasketLinesAdded is called.

Please refer to the test file https://github.com/folio-org/ui-agreements/blob/master/src/routes/AgreementEditRoute/AgreementEditRoute.test.js for more exmplaes

Important note: In the above scenario we are testing if the queryUpdateMock is what gets called, but ideally accordion to RTL philosophy, you test for what changes on the UI when the callback is invoked. If there is an option to test the visual changes on the UI based on the invoked callback, that should be preferred over testing out the implementation details of the callback.

Debugging

https://testing-library.com/docs/dom-testing-library/api-debugging/

TODO

FAQs

Q) What should I do if the screen.debug doesnt display the entire dom

...