By Carlos Bléon 15/08/2019

Mocking Functional React Components

React

Most of the time you either mount the component including all its inner components in order to test it or instantiate a shallow version of it. Sometimes however you need a mix, mounting the component for real except for one piece. This is when you need to mock out an inner component to spy on it or stub it’s behaviour.

Like in the previous post, I am using React Testing Library for this test. We had a component with a text editor inside. For the text editor we are using CKEditor. In the tests we didn’t know how to simulate the interaction with CKEditor so we decided to wrap it up with a component of our own that we can mock. This is our component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// NotesEditor.tsx file

import React from 'react';
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import language from "@ckeditor/ckeditor5-build-classic/build/translations/es";

function NotesEditor({textNote, onChange}) {
    return (
        <div data-testid="notes-editor">
            <CKEditor
                editor={ClassicEditor}
                config={{
                    toolbar: ['bold', 'italic', 'link', 'bulletedList', 'numberedList'],
                    language,
                }}
                data={textNote}
                onChange={(event, editor) => {
                    onChange(editor.getData());
                }}
            />
        </div>
    );
}

export default NotesEditor;

There are two interactions with this component:

In order inject the component I need to prepare the container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ContainerComponent.tsx file

import React, {useEffect, useState} from 'react';

interface ContainerComponentProperties {
    props: any,
    NotesEditor: any
}

function ContainerComponent({props, NotesEditor}: ContainerComponentProperties) {
    const [notes, setNotes] = useState(props.notes || []);
    const [currentNote, setCurrentNote] = useState({id:1, content: ""});

    /* ... some more javascript code goes here to handle notes ... */
    
    const onNoteChanged = (newText) => {
        setCurrentNote({
            id: currentNote.id,
            content: newText,
        });
    };

    return (
       /* ... some html here... */
        <div className="modal-content">
            <NotesEditor
                textNote={currentNote.content}
                onChange={onNoteChanged}
            />
        </div>    
     /* ... some html here... */                        
  )
}

The actual application is assembled like this:

1
2
3
4
5
6
7
8
9
// App.tsx file

import React from 'react'
import {render} from 'react-dom'
import {ContainerComponent} from './ContainerComponent';
import NotesEditor from "./components/NotesEditor/NotesEditor";

render(<ContainerComponent NotesEditor={NotesEditor}></CaseComponent>,
    document.querySelector('#Container'))

The ContainerComponent is bigger than what’s is shown in the snippet above, it manages a list of notes so that the user can add and edit notes. That code has been removed from the example for brevity.

We wanted to test that the user may select a note from a list of notes, and the text is rendered in the notes editor. On the other hand we wanted to test that once the note has changed in the editor it’s updated in the notes list, via the “currentNote” property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ContainerComponent.test.tsx file

function getNotesEditorSpy () {
    const spy = {
        receivedText: "",
        changeHandler: (text) => {},
        component: null
    };
    function FakeNotesEditor({textNote, onChange}) {
        spy.receivedText = textNote;
        spy.changeHandler = onChange;
        return (
            <div>
                <ul>
                    <li>`${textNote}`</li>
                </ul>
            </div>
        );
    }
    spy.component = FakeNotesEditor;
    return spy;
}

function renderComponent(props = {}, NotesEditor=getNotesEditorSpy().component) {
    return render(
        <ContainerComponent props={props} NotesEditor={NotesEditor}/>,
    );
}

/* other helper functions are omitted for brevity */

it("edits a note in the modal", async () => {
    let noteContent = "someNote for tests";
    let noteId = 77;
    const spyNotesEditor = getNotesEditorSpy();
    testHelper = renderComponent({}, {
        notes:[{id: noteId, content:noteContent}]},
        spyNotesEditor.component);

    simulateClickOnEditNoteButton(testHelper, noteId);

    expect(spyNotesEditor.receivedText).toEqual(noteContent);
});

it("modifies note when click on save", async () => {
    let noteContent = "someNote for tests";
    let newNoteContent = "the modified note";
    let noteId = 77;
    const spyNotesEditor = getNotesEditorSpy();
    testHelper = renderComponent({}, {
            notes:[{id: noteId, content:noteContent}]},
        spyNotesEditor.component);
    const notesList = await waitForNotesList();
    simulateClickOnEditNoteButton(testHelper, noteId);
    await waitForNotesModal();
    spyNotesEditor.changeHandler(newNoteContent);

    simulateClickOnSaveNoteButton(testHelper);

    expect(notesList.innerHTML).not.toContain(noteContent);
    expect(notesList.innerHTML).toContain(newNoteContent);
});

The trick here is to implement a fake version of the NotesEditor where we get the chance to intercept what text it’s receiving and also to get a reference to the onChange handler the ContainerComponent passes to it, so that we can invoke it in the tests to simulate a change in the text. That is all the getNotesEditorSpy does, create a component we can spy on.

When the ContainerComponent is rendered in the tests, everything is real except for the NotesEditor which is a fake. We are not testing the NotesEditor here, the tests care about the ContainerComponent. We would need additional tests for the NotesEditor or we could decide not to add integration tests for it - after all it’s not a component that we plan to change and if we do, we better test it manually using the app in a real browser.

Do you want more? We invite you to subscribe to our newsletter to get the most relevan articles.

If you enjoy reading our blog, could you imagine how much fun it would be to work with us? let's do it!