Saturday, June 18, 2016

TDD of a React-Redux connector

TLDR


React is a framework for building reusable components. Redux is a library to store and manage changes to web application state. Wouldn't it be nice to use them together? 

You might want to read a couple of previous blogs about react and redux first.

Design

We used the "react-redux" connector. This connector lets you wrap redux state and dispatch methods around a react component.

Test Strategy

I contemplated a few different ways to test react and redux. The two that seemed the most interesting were:
  • Perform a "shallow render" of the component and check appropriate parameters were being passed to the login form component.
  • Perform a full render of the component, trigger the submit button, and check that the store is appropriately modified.
While the first of those two is conceptually cleaner, I went with the second one due to context injection. Basically, by this stage I'd moved the loginService object to be passed as part of the react context instead of being a parameter - as passing the loginService as a parameter was starting to seem ugly. To inject the right context for the tests, I had to wrap the redux login form inside a context injector object. I also had to wrap the redux login form in a "provider" react-redux component. This is part of the react-redux library - and it provides the store to the components under it in the component tree.

As a side effect, the redux wrapper was not rendering as part of a shallow render. Sigh. 

So here are my imports and test setup method:

import {mount} from 'enzyme';
import ContextContainer from "./ContextContainer.jsx";
import LoginService from "../../src/services/LoginService.jsx"
import React from 'react';
import ReduxLoginForm from "../../src/components/ReduxLoginForm.jsx";
import {Provider} from "react-redux";
import {createStore} from "redux";
import reducers from "../../src/model/combinedReducers.jsx";


beforeEach(() => {
  loginService = new LoginService();
  store = createStore(reducers);

  loginForm = mount(
    <Provider store={store}>
      <ContextContainer loginService={loginService}>
        <ReduxLoginForm />
      </ContextContainer>
    </Provider>
  );
});

Tests

There was only one test for this component - that the form sends a dispatch method to the store if the login is successful. Note the way I'm mocking a successful promise - I found that using an actual promise object caused me issues related (I assume) to asynchronous behaviour. I'm also not happy with the way I'm invoking the form submission - it relies on knowledge about the login form, but this test is not for the login form.

it("generates a dispatch to the store on successful login", () => {
  var successPromise = { then: (s,r) => s("test token")};
  spyOn(loginService, "login").and.returnValue(successPromise);

  expect(store.getState().authentication.get("loggedIn"))
    .toEqual(false);

  loginForm.find("button.submitLogin").simulate("click");

  expect(store.getState().authentication.get("loggedIn"))
    .toEqual(true);

  expect(store.getState().authentication.get("token"))
    .toEqual("test token");
});

Code

In case you are interested, here is the code for the ReduxLoginForm

import LoginForm from "./LoginForm.jsx";
import {connect} from "react-redux";
import {createLoginAction} from "../model/authenticationReducer.jsx";

var dispatchMap = dispatch => {
  return {
    onLogin: (username, password) => {
      dispatch(createLoginAction(username, password));
    }
  };
};

var propsMap = () => {
  var props = {
  };
  return props;
};

var ReduxLoginForm = connect(propsMap, dispatchMap)(LoginForm);

export default ReduxLoginForm;


No comments: