TDD+React

 Will Vaughn

Software Engineer R/GA

@nackjicholsonn

 

github:nackjicholson

 

willieviseoae@gmail.com

 

william.vaughn@rga.com

TDD+React

  • Designing Components
  • Test Anatomy 101
  • React ecosystem testing libs
  • Test drive an easy component
  • Talk about stuff which is hard to test

Designing Components

Big components will hurt.

>3 Dependencies will crush you.

How will we possibly deal with all the well organized, carefully named, comprehensible small things?! 

Designing Components

Data => Display

Interaction => Action

Designing Components

Test Anatomy 101

import assert from 'assert';

describe('What unit aspect are you testing?', () => {
  it('What should the unit do?', () => {
    const actual = 'What is the actual output?';
    const expected = 'What do you expect the output to be?';

    assert.equal(actual, expected, 'Describe to me what happened here.');
  });
});

5 Questions

Test Anatomy 101

import assert from 'assert';
import shout from './shout';

describe('shout', () => {
  it('should uppercase a string', () => {
    // "Arrange"
    const str = 'foobar';

    // "Act"
    const actual = shout(str);
    const expected = 'FOOBAR';

    // "Assert"
    assert.equal(actual, expected, 'returns str uppercased');
  });
});

shout should uppercase a string

Testing Libararies

  • airbnb/enzyme
  • React Addons Test Utilities
  • jquense/teaspoon

airbnb/enzyme

  • Shallow, static, full rendering
  • Traversal helpers
  • Simulated events
  • A bit clunky
  • Issues with nested DOM elements
  • Issues with simulated events

ReactTestUtils

  • Official facebook support
  • Makes tests really verbose
  • Readability suffers
  • Light documentation

jquense/teaspoon

  • Full and shallow render
  • Fluent jQuery-like API
  • Nested DOM elements
  • Simulated events
  • Lesser known library

Presentational Component

Does it display the props we give it?

Does it respond to user interactions?

Stateless Functional Component

SelectControl

SelectControl HTML

<div id="robot-sounds-wrapper">
  <label for="robot-sounds-select">Robot Sounds:</label>
  <select id="robot-sounds-select" name="robot-sounds-select">
    <option value="beep">beep</option>
    <option value="boop" selected >boop</option>
  </select>
</div>
{
 id: 'robot-sounds'
 labelText: 'Robot Sounds:'
 defaultValue: 'boop',
 onSelection: (value) => { /* do something with value */ }
}

Props

HTML

SelectControl Outline

import $ from 'teaspoon';
import assert from 'assert';
import React from 'react';
import { spy } from 'sinon';
import SelectControl from './SelectControl';

describe('app/components/SelectControl', () => {
  it('should render wrapper div');
  it('should render label with labelText prop');
  it('should render children prop within select list');
  it('should be able to select a default option');
  it('should handle selection changes via onSelection callback prop');
});

SelectControl Wrapper

it('should render wrapper div', () => {
  // "Arrange"
  const props = { id: 'foo' };

  // "Act"
  const actual = $(<SelectControl {...props} />)
    .render()
    .find('div[id=foo-wrapper]')
    .length;
  const expected = 1;

  // "Assert"
  assert.equal(actual, expected, 'rendered 1 wrapper div based on id prop');
});

SelectControl Wrapper

import React from 'react';

export default function SelectControl(props) {
  return <div id={`${props.id}-wrapper`}></div>;
}

SelectControl <label>

it('should render label with labelText prop', () => {
  const props = { id: 'foo', labelText: 'test.labelText' };

  const actual = $(<SelectControl {...props} />)
    .render()
    .find('div > label[htmlFor=foo-select]')
    .text();
  const expected = 'test.labelText';

  assert.equal(actual, expected, 'label rendered with text from labelText prop');
});

SelectControl <label>

import React from 'react';

export default function SelectControl(props) {
  return (
    <div id={`${props.id}-wrapper`}>
      <label htmlFor={`${props.id}-select`}>{props.labelText}</label>
    </div>
  );
}

SelectControl children

it('should render children prop within select list', () => {
  const props = { id: 'foo' };

  const component = (
    <SelectControl {...props}>
      <option value="alpha.value">alpha.text</option>
      <option value="bravo.value">bravo.text</option>
      <option value="charlie.value">charlie.text</option>
    </SelectControl>
  );
  const $options = $(component)
    .render()
    .find('div > select[id=foo-select] option');

  const actual = $options
    .map(optionNode => [optionNode.value, optionNode.text])
    .get();
  const expected = [
    ['alpha.value', 'alpha.text'],
    ['bravo.value', 'bravo.text'],
    ['charlie.value', 'charlie.text']
  ];

  assert.deepEqual(actual, expected, 'rendered option children within select list');
});

SelectControl children

import React from 'react';

export default function SelectControl(props) {
  return (
    <div id={`${props.id}-wrapper`}>
      <label htmlFor={`${props.id}-select`}>{props.labelText}</label>
      <select id={`${props.id}-select`} name={`${props.id}-select`}>
        {props.children}
      </select>
    </div>
  );
}

SelectControl children

import React from 'react';

export default function SelectControl(props) {
  const { id, children, labelText } = props;

  const wrapperId = `${id}-wrapper`;
  const selectId = `${id}-select`;

  return (
    <div id={wrapperId}>
      <label htmlFor={selectId}>{labelText}</label>
      <select id={selectId} name={selectId}>
        {children}
      </select>
    </div>
  );
}

SelectControl defaultValue

it('should be able to select a default option', () => {
  const props = { id: 'foo', defaultValue: 'bravo.value' };

  const component = (
    <SelectControl {...props}>
      <option value="alpha.value">alpha.text</option>
      <option value="bravo.value">bravo.text</option>
      <option value="charlie.value">charlie.text</option>
    </SelectControl>
  );
  const $options = $(component)
    .render()
    .find('div > select[id=foo-select] option');

  const checkedOption = $options
    .get()
    .find(optionNode => optionNode.selected);

  const actual = checkedOption.value;
  const expected = 'bravo.value';

  assert.equal(actual, expected, 'selected the option via defaultValue prop');
});

SelectControl defaultValue

import React from 'react';

export default function SelectControl(props) {
  const { id, children, defaultValue, labelText } = props;

  const wrapperId = `${id}-wrapper`;
  const selectId = `${id}-select`;

  return (
    <div id={wrapperId}>
      <label htmlFor={selectId}>{labelText}</label>
      <select id={selectId} name={selectId} defaultValue={defaultValue}>
        {children}
      </select>
    </div>
  );
}

SelectControl onSelection

it('should handle selection changes via onSelection callback prop', () => {
  const props = { id: 'foo', onSelection: spy() };

  $(<SelectControl {...props} />)
    .render()
    .find('div > select[id=foo-select]')
    .trigger('change', { target: { value: 'bingo' } });

  assert(props.onSelection.calledOnce, 'onSelection callback prop called once');

  const actual = props.onSelection.args[0];
  const expected = ['bingo'];

  assert.deepEqual(
    actual,
    expected,
    'onSelection callback called with change value'
  );
});

SelectControl onSelection

import React from 'react';

export default function SelectControl(props) {
  const { id, children, defaultValue, labelText, onSelection } = props;

  const wrapperId = `${id}-wrapper`;
  const selectId = `${id}-select`;

  function handleChange(event) {
    onSelection(event.target.value);
  }

  return (
    <div id={wrapperId}>
      <label htmlFor={selectId}>{labelText}</label>
      <select
        id={selectId}
        name={selectId}
        defaultValue={defaultValue}
        onChange={handleChange}
      >
        {children}
      </select>
    </div>
  );
}

Up the difficulty!

Thank You!

@nackjicholsonn

TDD+React

By Will Vaughn

TDD+React

Outlined approach to testing react components with an emphasis on TDD.

  • 1,402