Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Outside In TDD with React and Redux

Outside In TDD with React and Redux

This was a talk I gave at React Boston 2017 on how to do outside in test driven development with React.

Brendan McLoughlin

September 24, 2017
Tweet

Other Decks in Programming

Transcript

  1. WHAT IS OUTSIDE IN TDD Technique for using tests to

    drive out the functionality of you app Build you app from the outside Mock the dependencies that don’t exist yet
  2. HOW DOES IT WORK IN REACT? Acceptance tests (Tests your

    features) Let your acceptance test fail while we implement our components and reducers Component Tests (Tests your components) Mock out the API you want redux to inject Mock out the API you want dependent components to expose Redux Tests Use your tests to design your the API of your action creators and selectors
  3. JEST No fuss test runner Out of the box assertions

    Great error messages Ships with create-react-app
  4. JEST it('should be null', () => { let n =

    null; expect(n).toBeNull(); expect(n).toBe(null); }); it('should be called', () => { let mock = jest.fn(); [1].map(mock); expect(mock).toHaveBeenCalled(); });
  5. JEST it('should be null', () => { let n =

    1; expect(n).toBeNull(); expect(n).toBe(null); }); it('should be called', () => { let mock = jest.fn(); // [1].map(mock); expect(mock).toHaveBeenCalled(); });
  6. ENZYME import { shallow } from 'enzyme'; it('renders three <Foo

    /> components', () => { const wrapper = shallow(<MyComponent />); expect(wrapper.find('Foo').length).toBe(3); expect(wrapper.find('.icon-star’).length).toBe(1); }); it('simulates click events', () => { const wrapper = shallow(<Foo onButtonClick={onButtonClick} />); wrapper.find('button').simulate('click'); });
  7. THE LAYERS INSIDE AN APP React UI Redux / Flux

    Domain Models Persistent State Business Logic Validation Formatting
  8. ISOLATED TESTS Component Tests Tests individual react components Redux Tests

    Tests the API your actions and selectors expose to the rest of you app Unit Tests Business Logic Validation Formatting
  9. COMPONENT TESTS Use Enzyme to query the render tree and

    assert the rendered state Test both sides of conditions inside render() Use simulate(event) to test that the correct behavior occurs in response to specific events
  10. it('should not render without an episode’,()=> { let wrapper =

    shallow(<AudioPlayer />) expect(wrapper.html()).toBe(null) }) it('should render if there is an episode',()=> { let wrapper = shallow(<AudioPlayer episode={episode} />) let title = wrapper.find(‘.episode-title') expect(title.text()).toBe(episode.title) })
  11. it('should render pause button after ',()=> { let wrapper =

    shallow( <AudioPlayer episode={episode} /> ); wrapper.find(‘.play').simulate('click'); let pause = wrapper.find(‘.pause’); expect(pause.length).toBe(1); });
  12. ADVANCED COMPONENT TESTING Isolate your component tests from redux /

    flux Use defaultProps as a test seam Search for component names as strings
  13. Use Enzyme shallow() import { shallow, mount, static } from

    'enzyme'; it('renders MyComponent components', () => { // shallow just renders to the next Component boundary const wrapper = shallow(<MyComponent />); // mount renders the fill render tree const wrapper = mount(<MyComponent />); // static renders to a string const wrapper = static(<MyComponent />); });
  14. Use Enzyme shallow() import { shallow, mount, static } from

    'enzyme'; it('renders MyComponent components', () => { // shallow just renders to the next Component boundary const wrapper = shallow(<MyComponent />); // mount renders the fill render tree const wrapper = mount(<MyComponent />); // static renders to a string const wrapper = static(<MyComponent />); });
  15. Use Enzyme shallow() import { shallow, mount, static } from

    'enzyme'; it('renders MyComponent components', () => { // shallow just renders to the next Component boundary const wrapper = shallow(<MyComponent />); // mount renders the fill render tree const wrapper = mount(<MyComponent />); // static renders to a string const wrapper = static(<MyComponent />); });
  16. Use Enzyme shallow() import { shallow, mount, static } from

    'enzyme'; it('renders MyComponent components', () => { // shallow just renders to the next Component boundary const wrapper = shallow(<MyComponent />); // mount renders the fill render tree const wrapper = mount(<MyComponent />); // static renders to a string const wrapper = static(<MyComponent />); });
  17. defaultProps As Test Seams class AudioPlayer extends React.Component { playEpisode()

    { 
 this.props.audio.play(); } // … } AudioPlayer.defaultProps = { get audio() { return document.createElement(‘audio'); } };
  18. defaultProps As Test Seams it('should play the audio element’, ()

    => { let fakeAudio = { play: jest.fn(), }; let wrapper = shallow( <AudioPlayer episode={episode} audio={fakeAudio} />); wrapper.find(‘.play').simulate('click'); expect(fakeAudio.play).toHaveBeenCalled(); });
  19. defaultProps As Test Seams it('should play the audio element’, ()

    => { let fakeAudio = { play: jest.fn(), }; let wrapper = shallow( <AudioPlayer episode={episode} audio={fakeAudio} />); wrapper.find(‘.play').simulate('click'); expect(fakeAudio.play).toHaveBeenCalled(); });
  20. Use Strings When Searching import { shallow } from ‘enzyme';

    import Foo from './foo' it('renders three <Foo /> components', () => { const wrapper = shallow(<MyComponent />); expect(wrapper.find(Foo).length).toBe(3); expect(wrapper.find('Foo').length).toBe(3); });
  21. Use Strings When Searching import { shallow } from ‘enzyme';

    import Foo from './foo' it('renders three <Foo /> components', () => { const wrapper = shallow(<MyComponent />); expect(wrapper.find(Foo).length).toBe(3); expect(wrapper.find('Foo').length).toBe(3); });
  22. REDUX TESTS Test Redux in isolation from the rest of

    your app Make a createStore() helper for instantiating your store with all its reducers and middleware Test action creators, reducers, and selectors all at once
  23. it('should return saved queries', function() { let store = createStore();

    let episode = {}; store.dispatch({ type: 'UPDATE_RECORDS', query: 'recent', results: [episode], }); expect( selectQuery(store.getState(), 'recent') ).toEqual([episode]); })
  24. ACCEPTANCE TESTS Acceptance tests / End to end (e2e) tests

    High level feature tests Exercise the entire application Focus only on testing the happy path
  25. it('should show a list of episodes', async ()=>{ // visit

    helpers boots up the `<App>`, routes // to `/`, and waits for any data to load // before returning an enzyme mount() object let page = await visit('/') expect(page.find(‘.episode').length).toBe(4); let episode = page.find(‘.episode').first(); expect(episode.find(‘.title').text()) .toBe('7.08- The Political Question’); });
  26. import fetchvcr from 'fetch-vcr' let waitForAsync = fetchSpy(fetchvcr); const visit

    = (location = '/') => { window.fetch = fetchvcr; let wrapper = mount(<App />); return waitForAsync().then(() => { return wrapper; }); }
  27. SECRET TO ENJOYING TESTING Run tests for 1 file at

    a time npm run test src/audio-player.test.js jest src/audio-player.test.js
  28. OUTSIDE IN TDD IN ACTION Start with a failing acceptance

    test Drive out Component and Redux Tests Use our failing tests to help us implement out our code
  29. Failing Acceptance Test it('should play an episode', async () =>

    { let page = await visit('/'); page.find('.play').simulate('click'); let player = page.find('.audio-player'); expect(player).toHaveClassName('playing'); });
  30. const Episode = ({episode}) => ( <article className="episode"> <img src={episode.image}

    /> <div className="podcast">{episode.podcast}</ <div className="title">{episode.title}</div> </article> )
  31. it('should call playEpisode when clicked’,()=> { let playEpisode = jest.fn();

    let wrapper = shallow(<Episode episode={episode} playEpisode={playEpisode}/>); wrapper.find(‘.play').simulate('click'); expect(playEpisode) .toHaveBeenCalledWith(episode); });
  32. it('should call playEpisode when clicked’,()=> { let playEpisode = td.function()

    let wrapper = shallow(<Episode episode={episode} playEpisode={playEpisode}/>) wrapper.find('.play').simulate('click') td.verify(playEpisode(episode)) })
  33. const Episode = ({episode}) => ( <article className="episode"> <img src={episode.image}

    /> <div className="podcast">{episode.podcast}</ <div className="title">{episode.title}</div> <button className="play">Play</button> </article> )
  34. const Episode = ({episode, playEpisode}) => ( <article className="episode"> <img

    src={episode.image} /> <div className="podcast">{episode.podcast}</ div> <div className="title">{episode.title}</div> <button className="play" onClick={() => playEpisode(episode) } >Play</button> </article> )
  35. it('should set a current episode’,() => { let store =

    createStore(); let episode = {}; store.dispatch({ type: 'PLAY_EPISODE', episode, }); let state = store.getState(); expect(getPlayingEpisode(state)).toBe(episode) });
  36. it('should set a current episode’,() => { let store =

    createStore(); let episode = {}; store.dispatch({ type: 'PLAY_EPISODE', episode, }); let state = store.getState(); expect(getPlayingEpisode(state)).toBe(episode) });
  37. it('should set a current episode’,() => { let store =

    createStore(); let episode = {}; store.dispatch({ type: 'PLAY_EPISODE', episode, }); let state = store.getState(); expect(getPlayingEpisode(state)).toBe(episode) });
  38. it('should set a current episode’,() => { let store =

    createStore(); let episode = {}; store.dispatch({ type: 'PLAY_EPISODE', episode, }); let state = store.getState(); expect(getPlayingEpisode(state)).toBe(episode) });
  39. it('should set a current episode’,() => { let store =

    createStore(); let episode = {}; store.dispatch({ type: 'PLAY_EPISODE', episode, }); let state = store.getState(); expect(getPlayingEpisode(state)).toBe(episode) });
  40. export default function reducer(state, action) { switch (action.type) { case

    'PLAY_EPISODE': return { episode: action.episode, }; default: return state; } }
  41. const Episode = ({episode, playEpisode}) => (…) let mDispatch =

    { playEpisode: (episode) => ({ episode, type: ‘PLAY_EPISODE’ }) } export default connect(null, mDispatch)(Episode)
  42. const Episode = ({episode, playEpisode}) => (…) let mDispatch =

    { playEpisode: (episode) => ({ episode, type: ‘PLAY_EPISODE’ }) } export default connect(null, mDispatch)(Episode)
  43. // Export the unconnected component for testing export const Episode

    = ({episode, playEpisode}) => (…) let mDispatch = { playEpisode: (episode) => ({ episode, type: ‘PLAY_EPISODE’ }) } export default connect(null, mDispatch)(Episode)
  44. import Episode from ‘./episode' it('should call playEpisode when clicked’,()=> {

    let playEpisode = td.function() let wrapper = shallow(<Episode episode={episode} playEpisode={playEpisode}/>) wrapper.find('.play').simulate('click') td.verify(playEpisode(episode)) })
  45. import { Episode } from ‘./episode' it('should call playEpisode when

    clicked’,()=> { let playEpisode = td.function() let wrapper = shallow(<Episode episode={episode} playEpisode={playEpisode}/>) wrapper.find('.play').simulate('click') td.verify(playEpisode(episode)) })
  46. SUMMARY Write acceptance tests for your features Use your tests

    to help you discover the right API for your dependent components, action creators and selectors Run 1 test file at a time when developing