Zum Hauptinhalt springen

Unit-Tests

Bei jedem JDP-Projekt sind Funktionen für Unit-Tests integriert. Sie sollten sehen, dass @jutro/test in package.json im devDependencies-Abschnitt vorhanden ist.

Nachdem im Projekt npm install oder npm ci ausgeführt wurde, können Sie die Funktionen für Unit-Tests überprüfen, indem Sie den Befehl npm run test an der Eingabeaufforderung ausführen. Es wird davon ausgegangen, dass die ausgeführten Testfälle, alle Fehlertestfälle und am Ende ein Testabdeckungsbericht angezeigt werden.

Beachten Sie, dass es bei einigen Testmethoden zu TypeScript-Fehlern kommen kann, z. B. toBeInTheDocument() nach der Generierung.

Darüber hinaus sind viele der Assert-Methoden der Testing Library in der bereitgestellten Konfiguration nicht verfügbar (Jest Dom-bezogene Methoden.

Eine Jutro-App führt Jest über die react-app-rewired-Schnittstelle aus.

Unit-Tests ausführen​

Unsere App-Tests werden in Jest durchgeführt, daher kann es sehr nützlich sein, die Dokumentation zu Jest CLI Options zur Hand zu haben.

Sie können alle Unit-Tests in Ihrer Anwendung im Stammverzeichnis Ihres Projekts mit dem folgenden Befehl ausführen. Führen Sie Folgendes im Stammverzeichnis der Anwendung aus:

npm run test

Ein Blick auf diesen Befehl in package.json zeigt, dass Jutro CLI verwendet wird. Somit können auch alle Unit-Tests in der Anwendung auf diese Weise ausgeführt werden:

jutro app:tests

Der Hauptunterschied zwischen den beiden Befehlen oben ist das Flag --collectCoverage. Jest-bezogene Flags können direkt für diesen Befehl verwendet werden.

Weitere Informationen zur Verwendung von Jutro CLI für Tests finden Sie hier.

Das Flag --collectCoverage ist ein Alias von --coverage[=<boolean>] und gibt an, dass Informationen zur Testabdeckung erfasst und in der Ausgabe gemeldet werden sollen. Wenn Sie dieses Flag verwenden, wird ein Testabdeckungsbericht angezeigt.

Mit diesem Flag wird außerdem ein neuer Abdeckungsbericht generiert, den Sie durch Öffnen von coverage\lcov-report\index.html anzeigen können.

Tests für eine spezifische Datei ausführen​

Möglicherweise möchten Sie Tests für eine spezifische Datei oder Komponente ausführen, um den Testprozess zu beschleunigen.

Sie können nur einen Test wie folgt ausführen:

npm run test <TestFileName>
----
jutro app:tests <TestFileName>
----
jutro app:tests path/to/<TestFileName>.test.tsx

Denken Sie daran, dass bei npm run test <TestFileName> das Flag --collectCoverage. verwendet wird.

Wenn Sie mehrere Tests ausführen möchten, fügen Sie einfach so viele Testdateinamen wie gewünscht zu den obigen Befehlen hinzu.

Sie können Tests für eine spezifische Datei auch in Jest ausführen, indem Sie die Option --findRelatedTests gefolgt vom Pfad zur Zieldatei verwenden.

npm test -- --findRelatedTests path/to/your-test-file.test.js

Wenn Sie beispielsweise eine Testdatei unter src/js/components/Tools.test.tsx gespeichert haben, können Sie die Tests in dieser Datei mit dem folgenden Befehl ausführen:

npm test -- --findRelatedTests src/js/components/Tools.test.tsx

Dateikonventionen​

Fügen Sie den Komponenten und Seiten nach Bedarf Tests hinzu. Siehe die Beispiele in:

  • src/app/__tests__
  • src/Pages/CodelessForm/__tests__

Beispiel für Unit-Tests von Komponenten

Einige gängige Testfälle können zum Testen einer Komponente verwendet werden, z. B.:

  1. Testelement ist vorhanden / nicht vorhanden:
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  1. Testinhalt stimmt überein:
expect(screen.getByRole('heading').textContent).toBe('Hello World');
expect(screen.getByRole('buttonOne').textContent.toBe(getTranslation(messages.buttonTitle));
  1. Testwert ist true / false:
expect(screen.getByDisplayValue('flag').nodeValue).toBeTrue
expect(screen.getByDisplayValue('flag').nodeValue).toBeFalse
  1. Testen, ob die Funktion x-mal aufgerufen wurde:
const onChange= jest.fn();
render(<MyDropdown onChange={onChange} />);
userEvent.click(dropdown);
expect(onChange).toHaveBeenCalledTimes(1);

Zusätzliche Konfiguration für Unit-Tests​

Jest verfügt über eine vordefinierte Konfiguration für Jutro-Apps. Wenn Sie eigene Konfigurationsoptionen hinzufügen möchten, müssen Sie zunächst eine Datei mit dem Namen jest.config.js erstellen. Wenden Sie dann eine der beiden folgenden Methoden an:

  • Exportieren eines Objekts. Mit dieser Methode wird Ihre Konfiguration mit der Grundkonfiguration zusammengeführt.
jest.config.js
module.exports = {
setupFilesAfterEnv: [
require.resolve('src/setupTests.js'), // path to extra setup file.
],
};
  • Exportieren einer Funktion, wobei die Basiskonfiguration das erste Argument ist.
jest.config.js
module.exports = (basicConfig) => {
const newConf = { ...basicConfig };
newConf.setupFilesAfterEnv.push(require.resolve('src/setupTests.js'));
return newConf;
};

Helfer in Unit-Tests​

Das Jutro-Paket @jutro/test bietet eine Reihe von Helferfunktionen, die für die Durchführung von Tests nützlich sein können, insbesondere für benutzerdefinierte Komponenten.

Siehe Helfer bei Jutro-Tests.

Mock-Abhängigkeiten​

Mit Mock-Funktionen können Sie die Verknüpfungen zwischen Code testen, indem Sie die tatsächliche Implementierung einer Funktion löschen, Aufrufe der Funktion (und die in diesen Aufrufen übergebenen Parameter) erfassen und die Konfiguration von Rückgabewerten zur Testzeit zulassen.

In einigen Szenarios müssen Sie zudem ein Mocking der Abhängigkeiten durchführen, um unerwartetes Verhalten zu verhindern. Beispielsweise steht für den Unit-Test beim Aufrufen der Funktion schema getter aus dem Jutro Digital SDK nicht genügend Arbeitsspeicher zur Verfügung. Um Arbeitsspeicherverlust zu vermeiden, wird ein Mock für die Funktion schema getter erstellt.

Es gibt zwei Möglichkeiten, Mocks für Funktionen zu erstellen. Sie haben folgende Möglichkeiten:

  • Erstellen einer Mock-Funktion zur Verwendung im Testcode
  • Schreiben eines manuellen Mocks zum Überschreiben einer Modulabhängigkeit

Mocking von API-Aufrufen​

Für das Mocking von API-Aufrufen können Sie das Paket jest.mock verwenden. Weitere Informationen zum Modellieren mit Jest finden Sie in der Jest-Dokumentation zu Mock-Funktionen.

Ein Beispiel: Ihre Komponente zeigt einen Benutzer-Avatar an, der das Jutro-Transportpaket verwendet.

UserAvatar.js
import React, { useEffect, useState } from 'react';
import { createHttpRequest } from '@jutro/transport';

const baseUrl = 'https://jsonplaceholder.typicode.com';

const getUserData = () => {
return createHttpRequest(baseUrl, true).get('/users/1', {});
};

export const UserAvatar = () => {
const [name, setName] = useState();

useEffect(() => {
getUserData().then((userData) => {
setName(userData.name);
});
}, []);

return (
<div
style={{
fontSize: '2rem',
padding: '3rem',
display: 'flex',
gap: '1rem',
flexDirection: 'column',
}}>
<div>{name}</div>
</div>
);
};

Um einen Test für diese Komponente zu schreiben, mocken Sie die get()-Funktion von createHttpRequest:

UserAvatar.test.js
import React from 'react';
import { render, screen } from '@jutro/test';
import { UserAvatar } from '../UserAvatar';

const explorerName = 'Marco Polo';
jest.mock('@jutro/transport', () => ({
...jest.requireActual('@jutro/transport'),
createHttpRequest: () => ({
get: async () => ({
name: explorerName,
}),
}),
}));

describe('UserAvatar', () => {
it('Displays the name of the famous explorer', async () => {
render(<UserAvatar />);
expect(await screen.findByText(explorerName)).toBeInTheDocument();
});
});

Mocking-Konfiguration​

Um verschiedene Konfigurationswerte zu testen, können Sie jest.mock verwenden. Stellen Sie sich zum Beispiel eine Komponente namens ConfigReader vor, die Werte aus der Jutro-Konfiguration anzeigt:

ConfigReader.js
import React from 'react';
import { useTranslator } from '@jutro/locale';
import { getConfigValue } from '@jutro/config';

import messages from './ConfigReader.messages';

export const ConfigReader = () => {
const translator = useTranslator();

const configValue = getConfigValue('SOME_CONFIG', '<default value>');

return (
<div data-testid="config-display">
{translator(messages.displayConfigValue, { configValue })}
</div>
);
};

Sie können es mit dem folgenden Code testen:

ConfigReader.test.js
import React from 'react';
import { render, screen, getTranslation } from '@jutro/test';
import { getConfigValue } from '@jutro/config';

import { ConfigReader } from '../ConfigReader';

// Before running tests in this file we need to mock the `getConfigValue` function.
// This particular function requires a bit more handling, as it is used during setup
// and needs a basic implementation.
jest.mock('@jutro/config', () => ({
...jest.requireActual('@jutro/config'),
getConfigValue: jest.fn((_path, defaultValue) => defaultValue),
}));

describe('handling env variables', () => {
describe('using default values', () => {
beforeEach(() => {
getConfigValue.mockImplementation((_path, defaultValue) => defaultValue);
});

it('renders default config value', () => {
render(<ConfigReader />);

expect(
screen.getByText(getTranslation('Config value is: <default value>'))
).toBeInTheDocument();
});
});

describe('using mocked values', () => {
const mockedEnvVariables = {
SOME_CONFIG: '<mocked env var>',
};

beforeEach(() => {
getConfigValue.mockImplementation(
(path, defaultValue) => mockedEnvVariables[path] ?? defaultValue
);
});

it('renders mocked config value', () => {
render(<ConfigReader />);

expect(
screen.getByText(getTranslation('Config value is: <mocked env var>'))
).toBeInTheDocument();
});
});
});

Globales Mocking der Konfiguration​

Sie können die Konfiguration global für alle Tests in Ihrer App simulieren, indem Sie eine src/__mocks__/@jutro/config.js-Datei hinzufügen.

const actual = jest.requireActual('@jutro/config');

module.exports = {
...actual,
getConfigValue: (path, defaultValue) => {
if (
path === 'accessibleDisabled.all' ||
path === 'accessibleDisabled.button'
) {
return true;
}

return actual.getConfigValue(path, defaultValue);
},
};

Testen des Ein-/Ausblendens von Komponenten​

Um zu testen, ob etwas nach einer bestimmten Interaktion ein- oder ausgeblendet wird, verwenden wir ein Jutro-Modal:

MyModalExample.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslator } from '@jutro/locale';
import { ModalNext } from '@jutro/components';

import { messages } from './MyModalExample.messages';
import { OpenModalTrigger } from './OpenModalTrigger';

export const MyModalExample = () => {
const translator = useTranslator();

const [showModal, setShowModal] = useState(false);

return (
<React.Fragment>
<OpenModalTrigger
onClick={() => setShowModal((value) => !value)}
isOpen={showModal}
/>
<ModalNext isOpen={showModal}>
<span>{translator(messages.modalContent)}</span>
</ModalNext>
</React.Fragment>
);
};

MyModalExample.propTypes = {
title: PropTypes.string,
};

MyModalExample.defaultProps = {
title: 'sample',
};
OpenModalTrigger.js
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@jutro/components';

import { messages } from '../MyModalExample.messages';

export const OpenModalTrigger = ({ onClick, isOpen }) => (
<Button
data-testid="youDontNeedThisId"
onClick={onClick}>
{isOpen ? messages.hideModalButton : messages.showModalButton}
</Button>
);

OpenModalTrigger.propTypes = {
onClick: PropTypes.func,
isOpen: PropTypes.bool,
};

Sie können das Modal mit dem folgenden Code testen. Beachten Sie die Kommentare im Code, in denen spezifische Tests ausführlicher erläutert werden.

MyModalExample.test.js
import React from 'react';
import { render, screen, getTranslation, within } from '@jutro/test';
import userEvent from '@testing-library/user-event';

import { messages } from '../MyModalExample.messages';
import { MyModalExample } from '../MyModalExample';

describe('Testing something appears/disappears', () => {
it('does not show modal by default', () => {
render(<MyModalExample />);

expect(
screen.queryByText(getTranslation(messages.modalContent))
).not.toBeInTheDocument();
});

it('toggles modal after clicking the button', async () => {
render(<MyModalExample />);

// When testing for an element not being present on a page,
// be the least specific you can.
// Otherwise it's easy to write a test that doesn't work properly.
// Imagine you want to test that there are no modals on the page, so you wrote a test:
// "I expect there is no modal with title 'My modal' that is also blue."
// "Correct" (but you won't know that there is a modal, but red).
// If you had instead:
// "I expect there is no modal."
// The answer would be "Incorrect, there is a red modal" and you know to fix the component or test.
// In testing if something exists it's the opposite: provide as many details as it makes sense.
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.showModalButton),
})
);

expect(screen.getByRole('dialog')).toBeInTheDocument();

// Another thing:
// If you are testing for an element not being present,
// also include an opposite assertion with the same query.
// You might make a mistake and look for an element that could never exist,
// so the test will always pass.
// It's not enough to do:
// "I expect there is no 'thingumajig' on the page."
// "Correct, there is no 'thingumajig'."
// Besides that, you also need an opposite assertion:
// <try to enable the thing>
// "This time I expect there is 'thingumajig' on the page."
// "Incorrect, there is still no 'thingumajig'."
// This is how you know the element you are toggling is actually 'thingummy',
// and the test will catch when someone changes 'thingummy' to 'thingumabob' in the future.
userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.hideModalButton),
})
);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('modal contains the expected content', () => {
render(<MyModalExample />);

userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.showModalButton),
})
);

// if you have multiple similar elements found by the same query, `within` can help with that.
// Don't overuse within if you don't need to
expect(
within(screen.getByRole('dialog')).getByText(
getTranslation(messages.modalContent)
)
).toBeInTheDocument();
});
});

Selecting an option from a dropdown​

To test selecting from a DropdownSelectField, let's create the following component:

MyDropdown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { DropdownSelectField } from '@jutro/legacy/components';

import styles from './MyDropdown.module.scss';
import { messages } from './MyDropdown.messages';

const availableValues = [
{
code: 'option-1',
name: { id: 'option-1.name', defaultMessage: 'Option 1' },
},
{
code: 'option-2',
name: { id: 'option-2.name', defaultMessage: 'Option 2' },
},
{
code: 'option-3',
name: { id: 'option-another.name', defaultMessage: 'Another option' },
},
];

export const MyDropdown = ({ onValueChange }) => {
const [selectedValue, setSelectedValue] = useState(null);

return (
<DropdownSelectField
id="test-dropdown"
label={messages.label}
value={selectedValue}
onValueChange={(newValue) => {
setSelectedValue(newValue);
onValueChange?.(newValue);
}}
availableValues={availableValues}
/>
);
};

MyDropdown.propTypes = {
onValueChange: PropTypes.func,
};

You can use the following as a blueprint for your dropdown tests.

MyDropdown.test.js
import React from 'react';
import {
render,
screen,
getTranslation,
checkA11yViolations,
userEvent,
} from '@testing-library/user-event';

import { messages } from '../MyDropdown.messages';
import { MyDropdown } from '../MyDropdown';

describe('Testing a dropdown', () => {
it('exists', () => {
render(<MyDropdown />);

screen.getByRole('combobox', { name: getTranslation(messages.label) });
});

it('contains all expected options', () => {
const expectedOptions = [
getTranslation('Option 1'),
getTranslation('Option 2'),
getTranslation('Another option'),
];

render(<MyDropdown />);

const dropdown = screen.getByRole('combobox', {
name: getTranslation(messages.label),
});

userEvent.click(dropdown);

expectedOptions.forEach((expectedOption) => {
expect(
screen.getByRole('option', { name: expectedOption })
).toBeInTheDocument();
});
});

it('updates the value when selected', () => {
const onChange = jest.fn();

render(<MyDropdown onChange={onChange} />);

const dropdown = screen.getByRole('combobox', {
name: getTranslation(messages.label),
});

userEvent.click(dropdown);

const firstOption = screen.getByRole('option', {
name: getTranslation('Option 1'),
});
const secondOption = screen.getByRole('option', {
name: getTranslation('Option 2'),
});

userEvent.click(firstOption);

// assert
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('option-1');
expect(dropdown).toHaveValue('option-1');

userEvent.click(dropdown);
userEvent.click(secondOption);

// assert
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith('option-2');
expect(dropdown).toHaveValue('option-2');
});
});

Entering data in a DateInput​

To test DateInput, let's start with the following component:

MyDateComponent.js
import React, { useCallback, useState } from 'react';
import { DateInput } from '@jutro/components/new';
import { useTranslator } from '@jutro/locale';

import messages from './MyDateComponent.messages';

export const MyDateComponent = ({ onValueChange: onValueChangeProp }) => {
const translator = useTranslator();

const [value, setValue] = useState();

const onValueChange = useCallback(
(newValue) => {
setValue(newValue);
onValueChangeProp(newValue);
},
[onValueChangeProp]
);

return (
<DateInput
id="myDate"
value={value}
onValueChange={onValueChange}
label={messages.dateLabel}
/>
);
};

You can use the following tests as a blueprint:

MyDateComponent.test.js
import React from 'react';
import { render, screen, getTranslation } from '@jutro/test';
import userEvent from '@testing-library/user-event';

import { MyDateComponent } from '../MyDateComponent';

const selectDate = async (date) => {
const dateInput = screen.getByRole('textbox', {
name: getTranslation('Select your date'),
});

await userEvent.type(dateInput, date);
};

const selectTime = async (time) => {
const timeInput = screen.getByRole('textbox', {
name: /Select your time/,
});

// await userEvent.type(timeInput, time);

await userEvent.click(timeInput);

const timeOption = screen.getByRole('option', {
name: time,
});

await userEvent.click(timeOption);
};

const selectTimezone = async (timezone) => {
const timezoneInput = screen.getByRole('combobox', {
name: getTranslation('Select your timezone'),
});

await userEvent.click(timezoneInput);

const timezoneOption = screen.getByRole('option', {
name: getTranslation(timezone),
});

await userEvent.click(timezoneOption);
};

describe('MyDateComponent', () => {
it('updates date, time and timezone', async () => {
const onValueChange = jest.fn();

render(<MyDateComponent onValueChange={onValueChange} />);

await selectDate('10/30/1994');
await selectTime('06:30 PM');
await selectTimezone('Antarctica/Troll');

expect(onValueChange).toHaveBeenCalledWith({
datetime: { year: 1994, month: 9, day: 30, hour: 18, minute: 30 },
timezone: 'Antarctica/Troll',
});
});
});

Key testing points to consider​

  • Try to avoid getting the element by its ID: IDs can be changed easily, and sometime IDs are automatically generated, so get the particular element by getByRole, queryByRole, or even getByText and getByDisplayValue instead.

  • Test the output, not the implementation: Some functions have very complex logics inside, and may have multiple dependencies. So instead of testing the function logic, focus on the input and output.

  • Use translation keys for testing if content is matched to the display key: The translation can change, so when testing display keys, try not to hardcode the text. Use the getTranslation function provided by the Jutro test library to get the display key instead.

React Strict Mode​

Der React Strict Mode ist eine native React-Komponente, die zusätzliche Prüfungen ausführt, wenn sie in einer Entwicklungsumgebung aktiviert ist, und Ihnen hilft, häufig auftretende Fehler in Ihren Komponenten zu finden. Dies bietet die folgenden Vorteile:

  • Es prüft Ihre Komponenten auf die Verwendung veralteter APIs
  • Die Komponenten werden erneut gerendert, um Komponenten zu finden, die ihr Verhalten beim zweiten Rendern unbeabsichtigt ändern
  • Es wird ein zusätzlicher Einrichtungs- und Bereinigungszyklus für alle Effekte in Ihrem Code erneut ausgeführt

Um den React Strict Mode zu aktivieren, fügen Sie .env der Datei JUTRO_REACT_STRICT_MODE=true hinzu.

Für bereits vorhandene Anwendungen ist der Strict Mode nicht standardmäßig aktiviert. Bei allen neuen Jutro-Apps, die aus einer Jutro-App-Vorlage erstellt werden, wird die Umgebungsvariable in der .env-Datei aktiviert.

When JUTRO_REACT_STRICT_MODE=true is set in the .env file, it is read by the jutro app:tests command and passed to the Jutro render helper inside @jutro/tests. Then in render, the following check is run:

const isInReactStrictMode = process.env.JUTRO_REACT_STRICT_MODE === 'true';

configure({
reactStrictMode: isInReactStrictMode,
});
Note: Wenn JUTRO_REACT_STRICT_MODE=true in der .env-Datei festgelegt ist, können Sie den Befehl npm run test auch in der Befehlszeile ausführen, um Ihre App im strikten Modus auszuführen.