React Testing Components: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
Line 160: Line 160:


=Testing Component Events=
=Testing Component Events=
==Approach==
For test events take four steps
*Set props and render
*Find elements you need
*Trigger events on elements
*Make assertions against events produced
==Testing Library==
We can use this to test events. Here is a simple component which increments a count on press.
<syntaxhighlight lang="js">
import React, { useState } from 'react';
const Counter = () => {
    const [count, setCount] = useState(0);
    return (
        <p onClick={()=> { setCount(count+1); }}>
          {count} ah ah ah
        </p>
    );
};
export default Counter;
</syntaxhighlight>
==Example Test Counter (Testing Library)==
Put this here to not scare people and show simple testing is easy.
<syntaxhighlight lang="js">
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Counter from './Counter';
describe('Counter', () => {
    it("should start at zero", () => {
        const { queryByText } = render(
            <Counter />
        );
        const paragraph = queryByText(/ah ah ah/);
        expect(paragraph).toBeTruthy();
        expect(paragraph.textContent).toBe('0 ah ah ah');
    });
    it("should increment on click", () => {
        const { queryByText } = render(
            <Counter />
        );
        const paragraph = queryByText(/ah ah ah/);
        expect(paragraph.textContent).toBe('0 ah ah ah');
        fireEvent.click(paragraph);
        expect(paragraph.textContent).toBe('1 ah ah ah');
        fireEvent.click(paragraph);
        expect(paragraph.textContent).toBe('2 ah ah ah');
    });
});
</syntaxhighlight>
==Asyc Testing and Jest==
Testing Asynchronous code with Jest '''requires test methods to return a promose''' or they will always pass. Here is the counter again but using a timeout to simulate Async
<syntaxhighlight lang="js">
import React, { useState } from 'react';
const CounterAsync = () => {
    const [count, setCount] = useState(0);
    return (
        <p onClick={()=> { setTimeout(()=> { setCount(count+1); }, 1000)}}>
          {count} ah ah ah
        </p>
    );
};
export default CounterAsync;
</syntaxhighlight>
==Example Test Counter Async (Testing Library)==
To test async we need to wait for the results. We do this by using the wait function and await.
<syntaxhighlight lang="js">
import React from 'react';
import { render, fireEvent, wait } from '@testing-library/react';
import CounterAsync from './Counter.async';
describe('Counter', () => {
    it("should start at zero", () => {
        const { queryByText } = render(
            <CounterAsync />
        );
        const paragraph = queryByText(/ah ah ah/);
        expect(paragraph).toBeTruthy();
        expect(paragraph.textContent).toBe('0 ah ah ah');
    });
    it("should increment on click", async () => {
        const { queryByText } = render(
            <CounterAsync />
        );
        const paragraph = queryByText(/ah ah ah/);
        await wait(() => {
            expect(paragraph.textContent).toBe('0 ah ah ah');
        });
        fireEvent.click(paragraph);
        await wait(() => {
            expect(paragraph.textContent).toBe('1 ah ah ah');
        });
        fireEvent.click(paragraph);
        await wait(() => {
            expect(paragraph.textContent).toBe('2 ah ah ah');
        });
    });
});
</syntaxhighlight>
=Testing States and Effects=
=Testing States and Effects=

Revision as of 04:34, 10 July 2021

Introduction

Guiding principle for testing is

Testing Exactly what you are trying to test and nothing more
Never test through a UI component what can be tested some other way
Never test the same thing twice

DOM Overview

Definition

The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and objects. That way, programming languages can connect to the page.

A Web page is a document. This document can be either displayed in the browser window or as the HTML source. But it is the same document in both cases. The Document Object Model (DOM) represents that same document so it can be manipulated. The DOM is an object-oriented representation of the web page, which can be modified with a scripting language such as JavaScript.

Document Element

This is one element in a DOM e.g. a div.

innerHTML vs outterHTML

InnerHTML is used for getting or setting a content of the selected whilst the outerHTML is used for getting or setting content with the selected tag. This is a referred to a lot by Web developers so here is a picture to remind people of the difference.

Testing Rendering

React Testing Library

React Testing Library is one of the most well known. They say,

  • The more your tests resemble the way the software is used, the more confidence they can give you.
  • Encourages rendering components to DOM nodes,
  • Making asserts against DOM nodes


It's API tries to encourage tests to represent how the software is used by the user. For example these tests focus on how the user might access the screen, by label, by text

const r = render(<Message content="Some Stuff" isImportant={true} />);
r.getByLabelText('First Name')
r.getByText('2017-5-13')
r.getByTitle('introduction')
// Frown upon because as a user id are not visible but...
r.getByTestId('target')

Example Test

Here is a full example for testing the Date slider

The tests use the getByTestId and render API.

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'
import DateSlider from './DateSlider';
import {solToDate, dateToSol} from '../services/sols';

describe('DateSlider', () => {

    describe('render', () => {
        it("should return a container", () => {
            const { container } = render(
              <DateSlider earth_date="2017-5-13" onDateChanged={()=>{}} />
            );
            expect(container).toBeDefined();
            expect(container.outerHTML).toBe("<div><div class=\"Dateslider\"><div class=\"row\"><div class=\"col-12\" style=\"text-align: center;\"><label for=\"date\">Earth Day</label><p class=\"Dateslider-date\" data-testid=\"date-label\">2017-5-13</p></div></div><div class=\"row\"><div class=\"col-3\" style=\"text-align: right;\"><small>2004-01-05</small></div><div class=\"col-6 form-group\"><input data-testid=\"date-slider\" type=\"range\" id=\"date\" class=\"form-control\" min=\"1\" max=\"5746\" value=\"4878\"></div><div class=\"col-3\"><small>2019-09-28</small></div></div></div></div>");
          });

        it('should display the correct date', () => {
            const { getByTestId } = render(<DateSlider earth_date="2017-5-13" onDateChanged={()=>{}} />);
            const date = getByTestId("date-label");
            expect(date).toHaveTextContent("2017-5-13");
        });
        it('should have the correct slider position', () => {
            const { getByTestId } = render(<DateSlider earth_date="2017-5-13" onDateChanged={()=>{}} />);
            const input = getByTestId("date-slider");
            expect(input).toHaveValue(dateToSol("2017-5-13").toString());
        });
    });

    describe('click', () => {
        it('should publish the selected date', () => {
            const fn = jest.fn();
            const { getByTestId } = render(<DateSlider earth_date="2017-5-13" onDateChanged={fn} />);
            
            const input = getByTestId("date-slider");
            fireEvent.change(input, { target: { value: '3877' } });
            
            expect(fn.mock.calls.length).toBe(1);
            expect(fn.mock.calls[0][0]).toBe(solToDate(3877));
        });
    });
});

React Test Utilities

This library provides the function act() which provides a more realistic environment for rendering and updating componenemts.

Example Test Camera (React Test Utilities)

This example is testing a combobox
To use the act API it was noted

  • create a container and make sure you add it to the document
  • jest provides beforeEach and AferEach to prepare and tear down
  • act is the API to render and update components
  • remember the ReactDOM.render requires the container as an argument
  • in the after each we remove the container and reset to null


The tests will consist of testing

  • should render a select element (something selected)
  • the selector renders each of the options
  • it selects the one we ask it to select
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import CameraSelection from './CameraSelection';

describe('CameraSelection', () => {

    describe('rendering', () => {
        let container,select;
        const cameras = {
            BC: "Big camera",
            LC: "Little camera"
        };
        beforeEach(() => {
            container = document.createElement('div');
            document.body.appendChild(container);
            ReactTestUtils.act(()=>{
                ReactDOM.render(<CameraSelection camera="LC" cameras={cameras} onCameraSelected={()=>{}} />, container);
            });
            select = container.querySelector('select');
          });
          
          afterEach(() => {
            document.body.removeChild(container);
            container = null;
          });

        it("should render a select element", () => {
            expect(select).toBeDefined();
        });
        it("should have 2 options", ()=>{
            expect(select.length).toBe(2);
        });
        it("should have LC (Little Camera) selected", ()=>{
            expect(select.value).toBe("LC");
            expect(select.selectedOptions[0].text).toBe('Little camera');
        });
    });
});

Test Render

Test Render does not use the DOM but instead test the javaScript objects. One of the great points which was made is you can identify the component is too complex when looking at testing and it may show opportunity to refactor unnecessarily complete code.

Example Test (Test Render)

Here we create an instance of the component and then look for if the checkbox is checked. It did appear to rely on data-testid prior to writing of testing.

            describe('none selected', ()=>{
                it('should select no rovers', ()=> {
                    const none = {spirit: false, opportunity: false, curiosity: false};
                    const tr = TestRenderer.create(<RoverSelector roversActive={none} rovers={rovers} roverSelection={none} onRoverSelection={()=>{}} />);
                    const inputs = tr.root.findAllByProps({"data-testid": 'rover-selected'});
                    inputs.forEach((input) => {
                        expect(input.props.checked).toBe(false);
                    });
                });
            });

Testing Component Events

Approach

For test events take four steps

  • Set props and render
  • Find elements you need
  • Trigger events on elements
  • Make assertions against events produced

Testing Library

We can use this to test events. Here is a simple component which increments a count on press.

import React, { useState } from 'react';

const Counter = () => {
    const [count, setCount] = useState(0);

    return (
        <p onClick={()=> { setCount(count+1); }}>
           {count} ah ah ah
        </p>
    );
};
export default Counter;

Example Test Counter (Testing Library)

Put this here to not scare people and show simple testing is easy.

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Counter from './Counter';

describe('Counter', () => {

    it("should start at zero", () => {
        const { queryByText } = render(
            <Counter />
        );
        const paragraph = queryByText(/ah ah ah/);
        expect(paragraph).toBeTruthy();
        expect(paragraph.textContent).toBe('0 ah ah ah');
    });

    it("should increment on click", () => {
        const { queryByText } = render(
            <Counter />
        );
        const paragraph = queryByText(/ah ah ah/);
        expect(paragraph.textContent).toBe('0 ah ah ah');
        fireEvent.click(paragraph);
        expect(paragraph.textContent).toBe('1 ah ah ah');
        fireEvent.click(paragraph);
        expect(paragraph.textContent).toBe('2 ah ah ah');
    });
});

Asyc Testing and Jest

Testing Asynchronous code with Jest requires test methods to return a promose or they will always pass. Here is the counter again but using a timeout to simulate Async

import React, { useState } from 'react';

const CounterAsync = () => {
    const [count, setCount] = useState(0);

    return (
        <p onClick={()=> { setTimeout(()=> { setCount(count+1); }, 1000)}}>
           {count} ah ah ah
        </p>
    );
};

export default CounterAsync;

Example Test Counter Async (Testing Library)

To test async we need to wait for the results. We do this by using the wait function and await.

import React from 'react';
import { render, fireEvent, wait } from '@testing-library/react';
import CounterAsync from './Counter.async';

describe('Counter', () => {

    it("should start at zero", () => {
        const { queryByText } = render(
            <CounterAsync />
        );
        const paragraph = queryByText(/ah ah ah/);
        expect(paragraph).toBeTruthy();
        expect(paragraph.textContent).toBe('0 ah ah ah');
    });

    it("should increment on click", async () => {
        const { queryByText } = render(
            <CounterAsync />
        );
        const paragraph = queryByText(/ah ah ah/);
        await wait(() => {
            expect(paragraph.textContent).toBe('0 ah ah ah');
        });
        fireEvent.click(paragraph);
        await wait(() => {
            expect(paragraph.textContent).toBe('1 ah ah ah');
        });
        fireEvent.click(paragraph);
        await wait(() => {
            expect(paragraph.textContent).toBe('2 ah ah ah');
        });
    });
});

Testing States and Effects