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. 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
  2. 4.

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

    public boolean submit(String orderId) { } private boolean validate(Order order) { } }
  3. 5.
  4. 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
  5. 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.
  6. 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
  7. 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
  8. 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
  9. 15.

    // CartComponent.jsx class Cart extends React.Component { componentDidMount() { }

    handleClick() { } render() { } } export default Cart
  10. 16.

    // CartContainer.js function mapStateToProps(state) { } function mapDispatchToProps(dispatch) { }

    export default connect( mapStateToProps, mapDispatchToProps )(Cart)
  11. 18.
  12. 19.
  13. 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)
  14. 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) }) })
  15. 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
  16. 23.

    Redux has changed how we think about single page apps,


    
 now we need to rethink testing. RE-EXAMINE THE UNIT
  17. 24.

    The unit is based on your understanding of the system

    and its testing.
 The view is the surface area. RE-EXAMINE THE UNIT
  18. 25.

    REACT REDUX OUR UNDERSTANDING OF THE SYSTEM VIEW AS A

    FUNCTION OF STATE FUNCTION STATE VIEW REACT DOM
  19. 26.

    VIEW AS THE UNIT 1-WAY CONTRACT = LOW SURFACE AREA

    Cart ProductPrice BuyButton OrderConfirmation STATE
  20. 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.') }) })
  21. 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.
  22. 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
  23. 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!') }) })
  24. 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.