Showing posts with label react. Show all posts
Showing posts with label react. 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;


Friday, June 17, 2016

TDD of a react component


TLDR: 

Building a react component using TDD forces you to focus on the component's interface - and ends up with a cleaner, more well defined component. However, the JavaScript space is so complex that figuring out how to write the tests is a project in itself. This blog explains bits of what you need.

The Tests

Recently at work, I've had to learn the React framework (as well as Redux and a few others). After a bit of googling, the sensible place to start was writing a simple login component. The role of this component is to show two fields: username and password, and submit the data to a loginService (I come from a back end development perspective and have developed a habit of wrapping external integration points in a service).

The tests I decided on were:

  • The component has a username field
  • The component has a password field
  • The component has a submit button
  • The component calls the login service's login method when submit is pressed
  • The component passes the value of username to the login service
  • The component passes the value of password to the login service
  • If login is successful then the component redirects to "/app"
  • If the login is not successful then the component displays an error.
These tests can, broadly, be broken down into three categories: structural, interaction start, and interaction end.

Test Setup

For those who are interested, here are my imports:
import React from 'react';
import LoginForm from '../../src/components/LoginForm.jsx';
import {mount} from 'enzyme';
import LoginService from "../../src/services/LoginService.jsx"

I use Karma/Jasmine for the test runner and, in addition, have webpack setup to produce a deployable bundle including the tests so I can run the tests in my user's web browser if they are having issues. That tells me which tests are failing in their browser with their set of plugins, configuration, etc, etc.

Structural

My initial approach to writing the structural test was:
  • Render the component using ReactTestUtils
  • Extract the DOM component using react-dom
  • Wrap the DOM in a jQuery object
  • Find the appropriate field and assert there is one of it.
However, after writing the test that way, I found out about the enzyme library. Enzyme, essentially, wraps the first 3 steps into 1 call.

For field tagging, I decided to use the "class" attribute. Could have used "id" attribute instead, but class made sense at the time.

My first test looked something like this:
it("Has a username field", () => {
  // setup
  var noop = () => {};
  var loginForm = mount(<LoginForm/>);

  // test and asserts
  expect(loginForm.find("input.username").length).toEqual(1);
});

Interaction Start

Here I quickly found that setting the value of a field in React/Enzyme is easy. Well, it's easy after an hour or so of using google. Note that I haven't written the loginService yet, so I'm just creating a mock object. Which I'd do anyway - because otherwise this test is not self-contained. Note that I eventually refactored this code to pass the LoginService object as part of the React context. That ended up being a nicer way to inject global singletons as dependancies - passing all of them pervasively through the react tree was going to be a lot of overhead for little gain.
it("Passes the username to the defined login method", () => {
  // setup
  var successPromise = { then: (s,r) => s("test token")};
  var loginService = new LoginService();
  spyOn(loginService, "login").and.returnValue(successPromise);
  var loginForm = mount(<LoginForm loginService={loginService}/>);
  var usernameField = loginForm.find("input.username");
  usernameField.simulate("change", {target: {value: "username"}});

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

  // asserts
  expect(loginService.login).toHaveBeenCalledWith("username", jasmine.anything());
});

Interaction End

After some thought about this test, I came to the conclusion that the login form should not be responsible for the decision of what to do after the login succeeds. So I added a parameter for the "loginSuccess" function and verified that the function was called.

it("Passes the onLogin method to the 'then' promise returned by the login service", (done) => {
  // asserts
  var loginSuccess = token => {
    expect(token).toEqual("test token");
    done();
  };

  // setup
  var successPromise = { then: (s,r) => s("test token")};
  var loginService = new LoginService();
  spyOn(loginService, "login").and.returnValue(successPromise);
  loginForm = mount(
    <LoginForm onLogin={loginSuccess} loginService={loginService}/>;

  // test
  loginForm.find("button.submitLogin").simulate("click");
});