Showing posts with label redux. Show all posts
Showing posts with label redux. Show all posts

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;


Test Driven Development of a Redux Store

TLDR:

Redux is a JavaScript library that is really useful to store and manage state of a application. It has 3 important concepts:
  • The state is the current snapshot of the data.
  • A store contains the current state.
  • A reducer takes an event and produces a new snapshot of the data
Externally, Redux is based on the "Observable" pattern (the "M" of MVC) where interested objects register as being interested when the store changes state. Internally, it contains a current state that does not change and does not provide setters on the state. 

Instead, to change the state you pass an event to the redux store (like "item deleted"). The store itself then creates an entirely new state by invoking all the reducers and notifies observers.

A really useful thing from a TDD perspective is that a store can have multiple reducers. Each reducer is responsible for managing an isolated part of the state - and events are sent to all reducers. This is useful because we can test reducers independently.

The Tests

To continue the example from the React Login Page, I decided to first build a simple store for authentication data. It needed to store a boolean "loggedIn" flag and an authentication token. After a bit of thought, I decided on the following tests:
  • Default value of logged in and token flags
  • Processing a "loggedIn" event should set the flag and token
  • Processing a "loggedOut" event should set the flag and remove the token.
  • The state is immutable
(note that each of those is actually two tests).

For those who are interested, my imports are:
import { createStore } from 'redux';
import reducer, {createLoginAction, createLogoutAction} from "../../src/model/authenticationReducer.jsx";

The rationale behind those imports is:
  • I want to export the reducer by default - not a created store. 
    • This lets me combine the reducer into a bigger store elsewhere in my application. 
    • It also means that I have to create the store in my app - so that the module can't return a hacked store. It has to return a proper reducer.
    • Finally, it means I don't have to test that the store being returned does the event notification properly.
  • I felt that the methods to create the login and logout actions belonged in the same file as the reducer.
    • That way I can have private constants for the events and all logic is in the same place. 
    • It also means my tests don't need to know the internals of the events.
In addition, I used the javascript "Immutable" library as it makes copying an entire state easy.

Default Values

This test is reasonably trivial - but important. By default the user should be logged out.

it("should be logged out by default", () => {
  var store = createStore(reducer);
  expect(store.getState().get("loggedIn")).toEqual(false);
  expect(store.getState().get("token")).toBeUndefined();
});

Logging In

Again, this is a reasonably trivial test.

it("should update the logged in flag and the token on an incoming 'login' event", () => {
  var store = createStore(reducer);
  store.dispatch(createLoginAction("12345"));

  expect(store.getState().get("loggedIn")).toEqual(true);
  expect(store.getState().get("token")).toEqual("12345");
});

Logging Out

Logging out was marginally more interesting because I had to get the store into a logged in state first. While I don't like calling the login method - because it means that a broken login could break this test - it seemed the cleanest way to achieve my need.

it("should update the logged in flag and the token on an incoming 'logout' event", () => {
  var store = createStore(reducer);

  // have to log in first!
  store.dispatch(createLoginAction("12345"));

  store.dispatch(createLogoutAction());

  expect(store.getState().get("loggedIn")).toEqual(false);
  expect(store.getState().get("token")).toBeUndefined();
});

Immutability

One of the principles of Redux is that the state itself does not change - instead the reducer produces a new, modified, state. To test this, I changed the "modified" test to keep a reference to the store post-login then check the state has not changed after logout.

it("should not modify the previous state", () => {

  // have to log in first!
  store.dispatch(createLoginAction("12345"));
  var loggedInState = store.getState();

  store.dispatch(createLogoutAction());

  expect(store.getState().get("loggedIn")).toEqual(false);
  expect(store.getState().get("token")).toBeUndefined();
      
  expect(loggedInState.get("loggedIn")).toEqual(true);
  expect(loggedInState.get("token")).toEqual("12345");
});

The Code

In case you are interested, here is the code for the login reducer:

import Immutable from "immutable";

const defaultState = new Immutable.Map({
  loggedIn: false
});

const LOGIN_ACTION = "authentication.LOGGED_IN";
const LOGOUT_ACTION = "authentication.LOGGED_OUT";

export default (state = defaultState, action) => {
  switch (action.type) {
    case LOGIN_ACTION:
      return state.set("loggedIn", true).set("token", action.token);

    case LOGOUT_ACTION:
      return state.set("loggedIn", false).set("token", undefined);

    default:
      // console.log("Ignoring action: " + action.type);
      return state;
  }
};

export const createLoginAction = token => {
  return {
    type: LOGIN_ACTION,
    token: token
  };
};

export const createLogoutAction = () => {
  return {
    type: LOGOUT_ACTION
  };
};