Behavioral Testing in React and Redux

Behavioral Testing in React and Redux

Re-examining the unit test.

In traditional object-oriented design, writing a “unit test” means instantiating your class and testing each method one by one, usually mocking out all other dependencies in the system. In a React application following the Flux architecture, the basic assumptions about what a unit test is changes. I will examine the architectural principles enabling this paradigm shift, and what testing in a React/Redux can look like.

Fd8e3ace64d471302758efb64e1eb0aa?s=128

Ryan Oglesby

October 11, 2017
Tweet

Transcript

  1. BEHAVIORAL TESTING IN REACT & REDUX RE-EXAMINING THE UNIT TEST

  2. WHAT DO WE WANT OUT OF OUR TESTS? ▸ Confidence

    ▸ The application works as expected ▸ Refactor without changing functionality or tests
 ▸ Documentation ▸ Use the language of the business ▸ Demonstrate conformance to business objectives (user flows, journeys)
 ▸ Act like an application ▸ Run fast ▸ Easy to maintain
  3. LET’S TALK ABOUT OBJECT-ORIENTED UNIT TESTING

  4. public class OrderService { public Order open(OrderData newOrderData) { }

    public boolean submit(String orderId) { } private boolean validate(Order order) { } }
  5. Object-oriented design treats the class as the unit.
 The methods

    are the surface area. CLASS AS THE UNIT
  6. CLASS AS THE UNIT TREE OF CONTRACTS = HIGH SURFACE

    AREA Orders Controller Products Controller Order Service Product Service Product Order Product View Order View
  7. CLASS AS THE UNIT ORGANIZE BY LAYER

  8. CLASS AS THE UNIT WHAT HAPPENS WHEN WE TEST LIKE

    THIS? ▸ Establish contracts between classes. ▸ Internal refactoring is easy. Surface area refactoring can quickly have ripple effects. ▸ Tests run very fast if providing test doubles for expensive dependencies. ▸ Balance speed vs coverage. ▸ Need wider tests to cover the interaction patterns of multiple classes.
  9. LET’S TALK ABOUT REACT & REDUX

  10. REACT REDUX FLUX/REDUX ARCHITECTURE COMPONENTS REDUCERS STORE dispatch actions provides

    state to
  11. REACT REDUX FLUX/REDUX ARCHITECTURE VIEW AS A FUNCTION OF STATE

    FUNCTION STATE VIEW REACT DOM
  12. REDUX FLUX/REDUX ARCHITECTURE VIEW AS A FUNCTION OF STATE STATE

    ‣100% of application state stored in the Redux store ‣State always comes from 1 location
  13. REACT FLUX/REDUX ARCHITECTURE VIEW AS A FUNCTION OF STATE FUNCTION

    ‣ Components are deterministic, stateless, predictable ‣ Components only have a contract with the Redux state
  14. FLUX/REDUX ARCHITECTURE VIEW AS A FUNCTION OF STATE VIEW REACT

    DOM ‣The View is a representation of the UI ‣React DOM is a trusted third party
  15. // CartComponent.jsx class Cart extends React.Component { componentDidMount() { }

    handleClick() { } render() { } } export default Cart
  16. // CartContainer.js function mapStateToProps(state) { } function mapDispatchToProps(dispatch) { }

    export default connect( mapStateToProps, mapDispatchToProps )(Cart)
  17. // cartReducer.js function addToCartAction(data) { } function cartReducer(state, action) {

    } export default cartReducer
  18. Object-oriented design treats the class as the unit.
 The methods

    are the surface area. CLASS AS THE UNIT
  19. React/Redux design treats the function as the unit.
 The functions

    are the surface area. FUNCTION AS THE UNIT
  20. // CartComponent.jsx class Cart extends React.Component { componentDidMount() { }

    _handleClick() { } render() { } } export default Cart // cartReducer.js function addToCartAction(data) { } function cartReducer(state, action) { } export default cartReducer // CartContainer.js function mapStateToProps(state) { } function mapDispatchToProps(dispatch) { } export default connect( mapStateToProps, mapDispatchToProps )(Cart)
  21. // cartActions.spec.jsx describe('Cart Action Creators', () => { it('creates an

    add to cart action', () => { const action = addToCartAction('a product name') expect(action).toEqual({ type: 'ADD_TO_CART', productName: 'a product name' }) }) }) // Cart.spec.jsx describe('Cart Component', () => { it('handles the click of the checkout button', () => { const cart = shallow( <Cart addToCartAction={stubAddToCart} dispatch={mockDispatch}> ).instance() cart.handleClick() expect(mockDispatch).toHaveBeenCalledWith(stubAddToCart) }) })
  22. FUNCTION AS THE UNIT WHAT HAPPENS WHEN WE TEST LIKE

    THIS? ▸ Small refactoring can produce false positives in tests. ▸ A lot of low value tests that must change often. ▸ Encourages unnecessary mocking of inexpensive, deterministic dependencies (Redux store) ▸ Tests don’t reflect business language or objectives. ▸ Need wider tests to cover interaction of component with its own container, actions, and reducer
  23. Redux has changed how we think about single page apps,


    
 now we need to rethink testing. RE-EXAMINE THE UNIT
  24. The unit is based on your understanding of the system

    and its testing.
 The view is the surface area. RE-EXAMINE THE UNIT
  25. REACT REDUX OUR UNDERSTANDING OF THE SYSTEM VIEW AS A

    FUNCTION OF STATE FUNCTION STATE VIEW REACT DOM
  26. VIEW AS THE UNIT 1-WAY CONTRACT = LOW SURFACE AREA

    Cart ProductPrice BuyButton OrderConfirmation STATE
  27. VIEW AS THE UNIT ORGANIZE BY COMPONENT

  28. VIEW AS THE UNIT BEHAVIORAL COMPONENT TEST // Cart.spec.jsx describe('Cart',

    () => { it('requires login before checking out’, { dispatch( addToCartAction('iPhone 8') ) const cart = shallow(<CartContainer />) cart.find('#checkout').simulate('click') expect(cart).toHaveText('You must login first.') }) })
  29. VIEW AS THE UNIT WHAT HAPPENS WHEN WE TEST LIKE

    THIS? ▸ Refactor confidently without changing tests. ▸ Test not opinionated about contents of Redux store, action shapes, or the names of props. ▸ More realistic tests without the need for mocks ▸ Better documentation of the business language and user flows ▸ Write fewer tests. Less code to maintain.
  30. VIEW AS THE UNIT WHAT ENABLES THIS KIND OF TESTING?

    ▸ Trust in the system and third parties ▸ Strict adherence to React & Redux principles ▸ Most components are connected directly to state ▸ Rarely pass props to children ▸ Most components to not hold local state (no setState) ▸ Enzyme and jsdom unlock fast access to the view
  31. SUBCUTANEOUS FUNCTIONAL TEST describe('Checkout', () => { jest.mock('../httpClient', stubHttpClient) it("allows

    a purchase after logging in", async () => { expect.assertions(1); const app = mount(<CartApp />) fillIn(app, '#search', 'iPhone 8') await httpReturns({name: 'iPhone 8', price: 899.99}) click(app, '#addToCart') click(app, '#checkout') fillIn(app, '#username', 'jane_doe') await httpReturns({success: true}) expect(app).toHaveText('Thanks for your purchase, Jane!') }) })
  32. THEMES ▸ The unit tests in a system are dependent

    on the paradigms and architecture of that system.
 ▸ Test observable behavior in the UI, not function return values. ▸ Save your mocks for the expensive things. ▸ Focus on enabling safe refactoring without being forced to change tests or wonder if things are working.
  33. Thanks! Ryan Oglesby
 roglesby@thoughtworks.com
 @ryanoglesby08