PHP & JavaScript engineer/ other nerd stuff
Currently: VP Engineering Experience SaturdayDrive.io
Find a bug or typo? Pull requests are welcome.
React creates an object representation of nodes representing a user interface.
It does not produce HTML.
React.createElement("div", { className: "alert" }, "Something Happened");
A "renderer" converts that object to a useable interface.
ReactDOM.render(<App />, domElement);
ReactDOMServer.renderToString(<App />);
Test Runner
Test Renderers
Assertions
react-scripts
react-scripts test
@wordpress/scripts
wordpress-scripts test
npx create-react-app
And A Web App :)
# install create-react-app npx create-react-app # Run the included test yarn test
Create React App comes with one test.
This is an acceptance test. It tests if anything is broken.
import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; it("renders without crashing", () => { const div = document.createElement("div"); ReactDOM.render(<App />, div); ReactDOM.unmountComponentAtNode(div); });
How do I know the components works?
How do I know the components work together?
What is the most realistic test of the program?
Create a one page app that:
Unit tests:
Integration Tests:
test()
Syntax//Import React import React from "react"; //Import test renderer import TestRenderer from "react-test-renderer"; //Import component to test import { DisplayValue } from "./DisplayValue"; test("Component renders value", () => {}); test("Component has supplied class name", () => {});
describe("EditValue Component", () => { //Shared mock onChange function let onChange = jest.fn(); beforeEach(() => { //Reset onChange mock before each test. onChange = jest.fn(); }); it("Has the supplied value in the input", () => {}); it("Passes string to onChange when changed", () => {}); });
yarn add react-test-renderer
//Probably don't do this test("Component renders value", () => { const value = "The Value"; const testRenderer = TestRenderer.create(<DisplayValue value={value} />); //Get the rendered node const testInstance = testRenderer.root; //find the div and make sure it has the right text expect(testInstance.findByType("div").props.children).toBe(value); });
Stores JSON in file system
Snapshots Acomplish Two Things:
test("Component renders correctly", () => { expect( TestRenderer.create( <DisplayValue value={"The Value"} className={"the-class-name"} /> ).toJSON() ).toMatchSnapshot(); });
React testing library is best for this. Enzyme is an alternative.
yarn add @testing-library/react
import { render, cleanup, fireEvent } from "@testing-library/react"; describe("EditValue component", () => { afterEach(cleanup); //reset JSDOM after each test it("Calls the onchange function", () => { //put test here }); it("Has the right value", () => { //put test here }); });
const onChange = jest.fn(); const { getByTestId } = render( <EditValue onChange={onChange} value={""} id={"input-test"} className={"some-class"} /> ); fireEvent.change(getByTestId("input-test"), { target: { value: "New Value" } }); expect(onChange).toHaveBeenCalledTimes(1);
const onChange = jest.fn(); const { getByTestId } = render( <EditValue onChange={onChange} value={""} id={"input-test"} className={"some-class"} /> ); fireEvent.change(getByTestId("input-test"), { target: { value: "New Value" } }); expect(onChange).toHaveBeenCalledWith('New Value');
With React Testing Library
test("matches snapshot", () => { expect( render( <EditValue onChange={jest.fn()} value={"Hi Roy"} id={"some-id"} className={"some-class"} /> ) ).toMatchSnapshot(); });
Do the two components work together as expected?
it("Displays the updated value when value changes", () => { const { container, getByTestId } = render(<App />); expect(container.querySelector(".display-value").textContent).toBe("Hi Roy"); fireEvent.change(getByTestId("the-input"), { target: { value: "New Value" } }); expect(container.querySelector(".display-value").textContent).toBe( "New Value" ); });
Using dequeue's aXe
# Add react-axe yarn add react-axe --dev # Add react-axe for Jest yarn add jest-axe --dev
This does NOT mean your app is accessible!
import React from "react"; import server from "react-dom/server"; import App from "./App"; import { render, fireEvent, cleanup } from "@testing-library/react"; const { axe, toHaveNoViolations } = require("jest-axe"); expect.extend(toHaveNoViolations); it("Raises no a11y errors", async () => { const html = server.renderToString(<App />); const results = await axe(html); expect(results).toHaveNoViolations(); });
Create a one page app that:
yarn add @wordpress/scripts
And A Plugin
A block for showing some text.
Will Gutenberg be able to manage our component’s state?
Does our plugin activate without errors?
Does our block appear in the block chooser?
@wordpress/scripts
??Provides:
# Install WordPress scripts yarn add @wordpress/scripts
See README
{ "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", "test:e2e": "wp-scripts test-e2e --config e2e/jest.config.js", "test:unit": "wp-scripts test-unit-js --config jest.config.js", "env:start": "bash start.sh" } }
Testing works the same, we can use same renderers.
@wordpress/scripts
works on top of Jest, webpack, Babel, etc.
The file that builds the block to do nothing but build the block.
import { registerBlockType } from "@wordpress/blocks"; import { Editor } from "./components/Editor"; import { Save } from "./components/Save"; const blockConfig = require("../block.json"); const { name, title, attributes, category, keywords } = blockConfig; registerBlockType(name, { title, attributes, category, keywords, edit: props => <Editor {...props} />, save: props => <Save {...props} /> });
The edit and save callback are composed in separate files, importing components built for the app.
import React, { Fragment } from "react"; import { EditValue } from "./app/EditValue"; import { DisplayValue } from "./app/DisplayValue"; import { InspectorControls } from "@wordpress/block-editor"; export const Editor = ({ attributes, setAttributes, className, clientId }) => { //Change handler const onChange = value => setAttributes({ value }); //current value const { value } = attributes; return ( <Fragment> <InspectorControls> <EditValue className={`${className}-editor`} id={clientId} value={value} onChange={onChange} /> </InspectorControls> <DisplayValue value={value} className={className} /> </Fragment> ); };
describe("Editor componet", () => { afterEach(cleanup); it("matches snapshot", () => { const attributes = { value: "Hi Roy" }; const setAttributes = jest.fn(); expect( render( <Editor {...{ attributes, setAttributes, clientId: "random-id", className: "wp-blocks-whatever" }} /> ) ).toMatchSnapshot(); }); });
import React from "react"; import { DisplayValue } from "./app/DisplayValue"; export const Save = ({ attributes, className }) => { return <DisplayValue value={attributes.value} className={className} />; };
describe("Save componet", () => { afterEach(cleanup); it("matches snapshot", () => { const attributes = { value: "Hi Roy" }; expect( render( <Save {...{ attributes, clientId: "random-id", className: "wp-blocks-whatever" }} /> ) ).toMatchSnapshot(); }); });
Assuming that all of the components work, does the program function as expected.
Test like the user
To make things easier, add the WordPress e2e test utilities:
# Add e2e test utilities yarn add @wordpress/e2e-test-utils
A seperate Jest config is needed to make sure it does NOT run unit tests.
const defaultConfig = require("./node_modules/@wordpress/scripts/config/jest-unit.config.js"); module.exports = { //use the default from WordPress for everything... ...defaultConfig, //Except test ignore, where we need to ignore our e2e test directory testPathIgnorePatterns: ["/.git/", "/node_modules/", "<rootDir>/e2e"] };
This is based on WordPress core's e2e tests
Easiest if you have WordPress running locally in Docker like core does
Import helper functions from @wordpress/e2e-test-utils
import { insertBlock, getEditedPostContent, createNewPost, activatePlugin } from "@wordpress/e2e-test-utils";
describe("Block", () => { beforeEach(async () => { await activatePlugin("josh-jswp/josh-jswp.php"); }); it("Can add block", async () => { await createNewPost(); await insertBlock("Josh Block"); expect(await getEditedPostContent()).toMatchSnapshot(); }); });
e2e tests ensure that the system works toghether.
They are a compliment to less expensive unit/ integration tests.
Assume your components will be reused.
Start with unit tests on new projects.
For legacy projects, start with acceptance tests.
Do not forget to test for accesibility errors
Follow and learn from Kent C. Dodds