Component Tests

In notebook:
FrontEndMasters Intermediate React
Created at:
2019-06-26
Updated:
2019-08-15
Tags:
React libraries JavaScript testing

https://frontendmasters.com/courses/intermediate-react-v2/component-tests/

Testing SearchParams

  //    ****        __test__/SearchParams.test.js        ****


import React from "react";
import { render, fireEvent, cleanup } from "react-testing-library";
// Jest know that these to be mocked ↴
import pet, { _breeds, _dogs, ANIMALS } from "@frontendmasters/pet";
import SearchParams from "../SearchParams";

// a Jest function:
afterEach(cleanup);

test("SearchParams", async () => {
  // it will render in NodeJS, no jsdom or else used
  // make sure that This is a component (`<MYComponent/>`)
  const { container, getByTestId, getByText } = render(<SearchParams />);

  // the convention is to add a `data-test` attribute
  // on your DOM element
  const animalDropdown = getByTestId("use-dropdown-animal");
  // testing that all items are inserted plus one for the "all" option.
  expect(animalDropdown.children.length).toEqual(ANIMALS.length + 1);

  // writing the second test
  expect(pet.breeds).toHaveBeenCalled();
  const breedDropdown = getByTestId("use-dropdown-breed");
  expect(breedDropdown.children.length).toEqual(_breeds.length + 1);

  // a more advanced test
  // we will render,
  const searchResults = getByTestId("search-results");
  // test the resulting text on the "page"
  expect(searchResults.textContent).toEqual("No Pets Found");
  // then siulate a click (fireEvent from `react-testing-library`)
  fireEvent(getByText("Subit"), new MouseEvent("click"));
  // then see if it gets the data back from the API 
  // note, that there's a discrepancy between the async
  // call in the real component
  // versus the mock implementation which is synchronous
  // solution: he just changed to synchronous implementation
  // in the real component
  // instead of monkey patching the promises API 
  expect(pet.animals).toHaveBeenCalled();
  expect(searchResults.children.length).toEqual(_dogs.length);

  expect(container.firstChild).toMatchInlineSnapshot(`..long DOM tree here`);
});

Jest render in NodeJS without jsdom

It's from react-testing-library, that allows us to render in NodeJS without any slow tricks, like headless Chrome, jsdom, etc.

Make sure that this is a component (<MYComponent/>):

const { container, getByTestId, getByText } = render(<SearchParams />);

Accessing your DOM elements in tests

Brian recommends adding data-test attributes and using these to target DOM elements in your tests. It's better decouple your tests and other logic.

The you can access your target with getByTestId. It's coming from the react-testing-library, render method. const animalDropdown = getByTestId("use-dropdown-animal");

  --- a/src/useDropdown.js
+++ b/src/useDropdown.js
@@ -8,6 +8,7 @@ const useDropdown = (label, defaultState, options) => {
       {label}
       <select
         id={id}
+        data-testid={id}
         value={state}
         onChange={e => updateState(e.target.value)}
         onBlur={e => updateState(e.target.value)}

Can also finally add tests to the test script in package.json.

  --- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "@babel/plugin-proposal-class-properties": "^7.4.0",
     "@babel/preset-env": "^7.4.3",
     "@babel/preset-react": "^7.0.0",
+    "@types/jest": "^24.0.9",
     "babel-eslint": "^10.0.1",
     "cross-env": "^5.2.0",
     "eslint": "^5.12.1",
@@ -22,8 +23,10 @@
     "eslint-plugin-jsx-a11y": "^6.2.0",
     "eslint-plugin-react": "^7.12.4",
     "eslint-plugin-react-hooks": "^1.0.2",
+    "jest": "^24.1.0",
     "parcel-bundler": "^1.12.1",
-    "prettier": "^1.16.1"
+    "prettier": "^1.16.1",
+    "react-testing-library": "^6.0.0"
   },
   "scripts": {
     "clear-build-cache": "rm -rf .cache/ dist/",
@@ -31,7 +34,10 @@
     "dev:mock": "cross-env PET_MOCK=mock parcel src/index.html",
     "format": "prettier --write \"src/**/*.{js,jsx}\"",
     "lint": "eslint \"src/**/*.{js,jsx}\" --quiet",
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "jest",
+    "test:coverage": "jest --coverage",
+    "test:watch": "jest --watch",
+    "test:update": "jest -u"
   },
   "author": "Brian Holt <btholt+complete-intro-to-react@gmail.com>",
   "license": "Apache-2.0",

Async implementation vs sync mock

The implementation uses async and await, the mock is a synchronous function. For now, the easier solution was to remove the async implementation. Brian says that he had some discussions about this with the React team, who also uses a lot of async and await and they said that they are working on a better solution...

  --- a/src/SearchParams.js
+++ b/src/SearchParams.js
@@ -12,16 +12,16 @@ const SearchParams = () => {
   const [animal, AnimalDropdown] = useDropdown("Animal", "dog", ANIMALS);
   const [breed, BreedDropdown, updateBreed] = useDropdown("Breed", "", breeds);
 
-  async function requestPets() {
-    const { animals } = await pet.animals({
-      location,
-      breed,
-      type: animal
-    });
-
-    console.log("animals", animals);
-
-    setPets(animals || []);
+  function requestPets() {
+    pet
+      .animals({
+        location,
+        breed,
+        type: animal
+      })
+      .then(({ animals }) => {
+        setPets(animals || []);
+      });
   }
 
   useEffect(() => {

Finally

Add the data-test id on the target

  --- a/src/Results.js
+++ b/src/Results.js
@@ -3,7 +3,7 @@ import Pet from "./Pet";
 
 const Results = ({ pets }) => {
   return (
-    <div className="search">
+    <div className="search" data-testid="search-results">
       {!pets.length ? (
         <h1>No Pets Found</h1>
       ) : (