2
votes

I have a pure React-Redux application and it is working as expected.

The App.js

import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { Router, Route, Switch, Redirect } from "react-router-dom";

import history from "../history";
import LandingPage from "./home/LandingPage";
import { displayModules } from "../actions";
import Cart from "./home/Cart";

const App = () => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(displayModules());
  }, [dispatch]);

  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={LandingPage}></Route>
        <Route path="/cart" exact component={Cart}></Route>
        <Route render={() => <Redirect to="/" />} />
      </Switch>
    </Router>
  );
};

export default App;

The LandingPage has a nested component called Tile.

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Tile from "../common/Tile";

import { addItemToCart, displayCartContents } from "../../actions";
import "./LandingPage.css";

const LandingPage = () => {
  const modules = useSelector(state => state.data.modules);
  const cart = useSelector(state => state.data.cart);
  const dispatch = useDispatch();
  const addToCart = item => {
    dispatch(addItemToCart(item));
  };
  return (
    <div className="app">
      <div className="header">
        <div className="text">Insurance modules</div>
        <i
          className="shopping cart icon"
          onClick={() => {
            dispatch(displayCartContents());
          }}
        >
          <span className="badge">{cart.length}</span>
        </i>
      </div>
      <div className="body">
        {modules.map(module => (
          <Tile key={module.id} module={module} addToCart={addToCart}></Tile>
        ))}
      </div>
    </div>
  );
};

export default LandingPage;

Tile.js has a button which I want to test.

import React, { useState } from "react";

import "./Tile.css";

const Tile = props => {
  const { module, addToCart } = props;
  const [coverage, setCoverage] = useState(parseInt(module.coverageMax - module.coverageMin) / 2);
  const [price, setPrice] = useState((coverage * module.risk) / 100);
  return (
    <div className="tile">
      <div className="tile-description">
        <div>
          <i className={`${module.icon} icon`}></i>
        </div>
        <div className="tile-name">{module.name}</div>
        <div className="tile-risk">Risk(%): {module.risk}</div>
      </div>
      <div className="tile-footer">
        <div className="tile-range">
          <div className="field-label">
            Select Coverage: <span className="coverage-display">{coverage}</span>
          </div>
          <div className="slidecontainer">
            <span className="slider-step">{module.coverageMin}</span>
            <input
              type="range"
              min={module.coverageMin}
              max={module.coverageMax}
              value={coverage}
              className="slider"
              onChange={e => {
                setCoverage(e.target.value);
                setPrice((e.target.value * module.risk) / 100);
              }}
            ></input>
            <span className="slider-step">{module.coverageMax}</span>
          </div>
        </div>
        <div>
          PRICE at this Coverage:<span className="tile-price">{price}</span>
        </div>

        <button
          className="tile-button"
          onClick={() => {
            addToCart({
              id: module.id,
              name: module.name,
              coverage: coverage,
              price: price,
              timeStamp: Math.ceil(new Date().getTime() * Math.random() * Math.random())
            });
          }}
        >
          Add module to cart
        </button>
      </div>
    </div>
  );
};

export default Tile;

App.test.js works fine and I am able to find the nested Landing Page div by className prop.

import React from "react";
import configureStore from "redux-mock-store";
import { Provider } from "react-redux";
import renderer from "react-test-renderer";

import App from "../components/App";
import history from "../history";
import { displayModules } from "../actions";
import { DISPLAY_MODULES } from "../actions/types";

const mockStore = configureStore([]);

describe("App Component test", () => {
  let store = {};
  let wrappedComponent = {};
  const expectedActions = {
    type: DISPLAY_MODULES,
    payload: [
      {
        id: 0,
        icon: "bicycle",
        name: "Bike",
        coverageMin: 0,
        coverageMax: 3000,
        risk: 30
      },
      {
        id: 1,
        icon: "gem",
        name: "Jewelry",
        coverageMin: 500,
        coverageMax: 10000,
        risk: 5
      },
      {
        id: 2,
        icon: "microchip",
        name: "Electronics",
        coverageMin: 500,
        coverageMax: 6000,
        risk: 35
      },
      {
        id: 3,
        icon: "football ball",
        name: "Sports Equipment",
        coverageMin: 0,
        coverageMax: 20000,
        risk: 30
      }
    ]
  };
  beforeEach(() => {
    store = mockStore({
      data: {
        modules: [],
        cart: [],
        total: 0
      }
    });
    store.dispatch = jest.fn(displayModules);
    wrappedComponent = renderer.create(
      <Provider store={store}>
        <App />
      </Provider>
    );
  });

  it("should render with given state from Redux store", () => {
    expect(wrappedComponent.toJSON()).toMatchSnapshot();
  });
  it("should have an app from Landing Page", () => {
    expect(wrappedComponent.root.findByProps({ className: "app" })).toBeDefined();
  });

  it("should show landing page for default route", () => {
    *debugger;
    expect(wrappedComponent.root.findByProps({ className: "shopping cart icon" })).toBeDefined();*
  });
  it("should show cart page for /cart route", () => {
    history.push("/cart");
    expect(wrappedComponent.root.findByProps({ className: "backward icon" })).toBeDefined();
  });
  it("should redirect to landing page for unmatched 404 routes", () => {
    history.push("/someRandomRoute");
    expect(wrappedComponent.root.findByProps({ className: "shopping cart icon" })).toBeDefined();
  });
  it("should dispatch displayModules action on app mount", async () => {
    const actualAction = await store.dispatch();
    expect(actualAction).toEqual(expectedActions);
  });
});

But If you see the test debugger enter image description here

The children of div with className: body has no children. That is why it is not able to find the Tile component. Can you suggest why the children are null for the body? I have seen this before, even i tried with Enzyme i faced this issue. Since it is a Redux wrapped component the , i cant directly create the Landing page or Tile component for testing. How to test the nested items?

1
I cant directly create the Landing page or Tile component for testing- you could do it by injecting your dependencies rather than declaring them inside the componentArnaud Claudel
I am not sure how to inject the dependencies, can you suggest any example.Subhadip Pal
@arnaud I understand what you meant after a little research...but that means using Context instead of Redux or use some other way to handle component wiring using refractJs or some other new library...just for unit testing...well that may be a solution to redesign the whole app for unit testing...but is it a drawback of using Redux then?Subhadip Pal
Hi, sorry I didn't have much time. I'll try to write a proper example this week. The idea is to simply pass the redux functions as parameters instead of using declaring them in the component. That way, you can easily mock it and therefore test itArnaud Claudel
@ArnaudClaudel, thanks it will be great if you could let me know. I will check that out too.Subhadip Pal

1 Answers

4
votes

You are providing an empty array to modules inside redux state:

store = mockStore({
  data: {
    modules: [], // your modules is empty so no tiles will render
    cart: [],
    total: 0
  }
});

Another issue is that you mock store.dispatch so it no longer changes the redux store even if some action is dispatched:

store.dispatch = jest.fn(displayModules);

If you want to test that an action was dispatched you can use:

const actions = store.getActions()

Which will give you all actions which were dispatched.

If you want to test how your app renders based on your store data you can either:

  1. Setup the store in the test:
const existingModules = [ ... ]; // list of modules
store = mockStore({
  data: {
    modules: existingModules, 
    cart: [],
    total: 0
  }
});
  1. You can mock useSelector in your test:
const existingModules = [ ... ]; // list of modules
const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue(existingModules)