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

Testing in JavaScript

Testing in JavaScript

Introduction to how to test in JavaScript with focus on React.

Links from presentation:

- https://github.com/rstankov/talks-code
- https://github.com/facebook/jest
- https://github.com/jasmine/jasmine
- https://github.com/mochajs/mocha
- https://github.com/chaijs/chai
- https://github.com/sinonjs/sinon
- https://github.com/airbnb/enzyme/
- https://github.com/producthunt/chai-enzyme
- https://github.com/graphcool/chromeless

Books from presentation:

- Refactoring: Improving the Design of Existing Code
- Test Driven Development: By Example
- Growing Object-Oriented Software: Guided by Tests
- XUnit Test Patterns: Refactoring Test Code
- JavaScript Testing Recipes

Radoslav Stankov

August 20, 2017
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. • code task 1 • check task 1
 • code

    task 2 • check task 2 • check task 1
 • code task 3 • check task 3 • check task 2 • check task 1
 • code task 4 • check task 4 • check task 3 • check task 2 • check task 1 • code task 1 • code task 1 test • run tests
 • code task 2 • code task 2 test • run tests
 • code task 3 • code task 3 test • run tests
 • code task 4 • code task 4 test • run tests Manual Automatic
  2. • code task 1 • check task 1
 • code

    task 2 • check task 2 • check task 1
 • code task 3 • check task 3 • check task 2 • check task 1
 • code task 4 • check task 4 • check task 3 • check task 2 • check task 1 • code task 5 • check task 4 • check task 4 • check task 3 • check task 2 • code task 1 • code task 1 test • run tests
 • code task 2 • code task 2 test • run tests
 • code task 3 • code task 3 test • run tests
 • code task 4 • code task 4 test • run tests • code task 5 • code task 5 test • run tests Manual Automatic
  3. ! Safety net " Reduces bugs in new or existing

    features # Forces you to understand the features and communication between components $ Reduces the cost for new features % Making code testable, decreases complexity and increases modularity
  4. Unit Test Integration Test Acceptance Test Tests one object Tests

    one “component” Tests the whole stack Isolated with mocks Some times uses stubs Uses the UI
 Expresses domain terms Fast Slow Very slow Helps for good design Helps for verification Helps for verification
  5. Unit Test Integration Test Acceptance Test Tests one object Tests

    one “component” Tests the whole stack Isolated with mocks Some times uses stubs Uses the UI
 Expresses domain terms Fast Slow Very slow Helps for good design Helps for verification Helps for verification
  6. “I should have confidence, that when my test pass, my

    code is most probably is working.” Reliable
  7. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); });
  8. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); });
  9. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Test Suite
  10. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Sub Test Suite
  11. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Test Case
  12. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Assertion
  13. describe(Calculator, () => { describe('evaluate', () => { beforeEach(() =>

    { console.log('test case start'); }); beforeAfter(() => { console.log('test case end'); }); it('handles "1 + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); });
  14. describe(Calculator, () => { describe('evaluate', () => { beforeEach(() =>

    { console.log('test case start'); }); beforeAfter(() => { console.log('test case end'); }); it('handles "1 + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Setup
  15. describe(Calculator, () => { describe('evaluate', () => { beforeEach(() =>

    { console.log('test case start'); }); beforeAfter(() => { console.log('test case end'); }); it('handles "1 + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Teardown
  16. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); it('handles "2 - 1" input', () => { const calculator = new Calculator('2 - 1'); const value = calculator.evaluate(); expect(value).toEqual(1); }); it('handles "2 * 3" input', () => { const calculator = new Calculator('2 * 3'); const value = calculator.evaluate(); expect(value).toEqual(6); }); }); });
  17. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); it('handles "2 - 1" input', () => { const calculator = new Calculator('2 - 1'); const value = calculator.evaluate(); expect(value).toEqual(1); }); it('handles "2 * 3" input', () => { const calculator = new Calculator('2 * 3'); const value = calculator.evaluate(); expect(value).toEqual(6); }); }); });
  18. describe(calculate.name, () => { describe('operations', () => { it('handles noop',

    () => { expect(calculate('12')).toEqual('12'); }); it('handles "1 + 1" input', () => { expect(calculate('1 + 1')).toEqual('2'); }); it('handles "1 - 1" input', () => { expect(calculate('2 - 1')).toEqual('1'); }); it('handles "2 * 3" input', () => { expect(calculate('2 * 3')).toEqual('6'); }); }); });
  19. describe(calculate.name, () => { describe('operations', () => { it('handles noop',

    () => { expect(calculate('12')).toEqual('12'); }); it('handles "1 + 1" input', () => { expect(calculate('1 + 1')).toEqual('2'); }); it('handles "1 - 1" input', () => { expect(calculate('2 - 1')).toEqual('1'); }); it('handles "2 * 3" input', () => { expect(calculate('2 * 3')).toEqual('6'); }); }); });
  20. Test Driven Development 1 Write test ... for code which

    is not written yet 2 Write code ... as little as require for the test
  21. Test Driven Development 1 Write test ... for code which

    is not written yet 3 Refactor ... remove duplications 2 Write code ... as little as require for the test
  22. Test Driven Development 1 Write test ... for code which

    is not written yet 3 Refactor ... remove duplications 2 Write code ... as little as require for the test
  23. describe(calculate.name, () => { it('handles "1 + 1"'); it('handles "2

    - 1"'); it('handles "2 * 2"'); }); Code Test
  24. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"'); it('handles "2 * 2"'); }); Code Test ✗
  25. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"'); it('handles "2 * 2"'); }); export default function calculate(memory) { return 2; } Code Test ✔
  26. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"', () => { expect(calculate('2 - 1')).toEqual(1); }); it('handles "2 * 2"'); }); export default function calculate(memory) { return 2; } Code Test ✗
  27. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"', () => { expect(calculate('2 - 1')).toEqual(1); }); it('handles "2 * 2"'); }); export default function calculate(memory) { const [a, operand, b] = memory.split(' '); if (operand === '+') { return Number(a) + Number(b); } else { return Number(a) - Number(b); } } Code Test ✔
  28. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"', () => { expect(calculate('2 - 1')).toEqual(1); }); it('handles "2 * 2"', () => { expect(calculate('2 * 2')).toEqual(4); }); }); export default function calculate(memory) { const [a, operand, b] = memory.split(' '); if (operand === '+') { return Number(a) + Number(b); } else { return Number(a) - Number(b); } } Code Test ✗
  29. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"', () => { expect(calculate('2 - 1')).toEqual(1); }); it('handles "2 * 2"', () => { expect(calculate('2 * 2')).toEqual(4); }); }); export default function calculate(memory) { const [a, operand, b] = memory.split(' '); if (operand === '+') { return Number(a) + Number(b); } else if (operand === '-') { return Number(a) - Number(b); } else { return Number(a) * Number(b); } } Code Test ✔
  30. describe(calculate.name, () => { it('handles "1 + 1"', () =>

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"', () => { expect(calculate('2 - 1')).toEqual(1); }); it('handles "2 * 2"', () => { expect(calculate('2 * 2')).toEqual(4); }); }); const OPERATIONS = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, }; export default function calculate(memory) { const [a, operand, b] = memory.split(' '); const operation = OPERATIONS[operand]; return operation(Number(a), Number(b)); } Code Test ✔
  31. describe(calculate.name, () => { describe('operations', () => { it('handles noop',

    () => { expect(calculate('12')).toEqual('12'); }); it('handles "1 + 1" input', () => { expect(calculate('1 + 1')).toEqual('2'); }); it('handles "1 - 1" input', () => { expect(calculate('2 - 1')).toEqual('1'); }); it('handles "2 * 3" input', () => { expect(calculate('2 * 3')).toEqual('6'); }); }); describe('edge cases', () => { it('handles zero in input', () => { expect(calculate('1 + 0')).toEqual('1'); }); it('handles larger number in input', () => { expect(calculate('123 + 456')).toEqual(new String(123 + 456)); }); it('handles multiple operations input', () => { expect(calculate('1 + 1 + 1')).toEqual('3');
  32. it('handles "2 * 3" input', () => { expect(calculate('2 *

    3')).toEqual('6'); }); }); describe('edge cases', () => { it('handles zero in input', () => { expect(calculate('1 + 0')).toEqual('1'); }); it('handles larger number in input', () => { expect(calculate('123 + 456')).toEqual(new String(123 + 456)); }); it('handles multiple operations input', () => { expect(calculate('1 + 1 + 1')).toEqual('3'); }); it('handles incomplete operation input', () => { expect(calculate('1 + ')).toEqual('1'); }); it('handles negative values', () => { expect(calculate('-1 + 2')).toEqual('1'); }); it('raises on invalid operand', () => { expect(() => calculate('1 % 2')).toThrowError('Invalid "%" operand'); }); }); });
  33. export default class Calculator extends React.Component { state = {

    memory: '0', }; render() { return ( <Grid size="4"> <Grid.Column size="3"> <Memory> {this.state.memory.trim()} </Memory> <Button onClick={this.add('1')}>1</Button> <Button onClick={this.add('2')}>2</Button> <Button onClick={this.add('3')}>3</Button> <Button onClick={this.add('4')}>4</Button> <Button onClick={this.add('5')}>5</Button> <Button onClick={this.add('6')}>6</Button> <Button onClick={this.add('7')}>7</Button> <Button onClick={this.add('8')}>8</Button> <Button onClick={this.add('9')}>9</Button> <Button onClick={this.reset}>C</Button> <Button onClick={this.add('0')}>0</Button> <Button onClick={this.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button onClick={this.calculate}>=</Button> <Button onClick={this.add('+')}>+</Button> <Button onClick={this.add('-')}>-</Button> <Button onClick={this.add('*')}>*</Button> </Grid.Column>
  34. <Button onClick={this.add('2')}>2</Button> <Button onClick={this.add('3')}>3</Button> <Button onClick={this.add('4')}>4</Button> <Button onClick={this.add('5')}>5</Button> <Button onClick={this.add('6')}>6</Button>

    <Button onClick={this.add('7')}>7</Button> <Button onClick={this.add('8')}>8</Button> <Button onClick={this.add('9')}>9</Button> <Button onClick={this.reset}>C</Button> <Button onClick={this.add('0')}>0</Button> <Button onClick={this.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button onClick={this.calculate}>=</Button> <Button onClick={this.add('+')}>+</Button> <Button onClick={this.add('-')}>-</Button> <Button onClick={this.add('*')}>*</Button> </Grid.Column> </Grid> ); } add = value => () => this.setMemory(addToMemory(this.state.memory, value)); remove = () => this.setMemory(removeFromMemory(this.state.memory)); reset = () => this.setMemory('0'); calculate = () => this.setMemory(calculate(this.state.memory)); setMemory = memory => this.setState({ memory }); }
  35. }; render() { return ( <Grid size="4"> <Grid.Column size="3"> <Memory>

    {this.state.memory.trim()} </Memory> <Button onClick={this.add('1')}>1</Button> <Button onClick={this.add('2')}>2</Button> <Button onClick={this.add('3')}>3</Button> <Button onClick={this.add('4')}>4</Button> <Button onClick={this.add('5')}>5</Button> <Button onClick={this.add('6')}>6</Button> <Button onClick={this.add('7')}>7</Button> <Button onClick={this.add('8')}>8</Button> <Button onClick={this.add('9')}>9</Button> <Button onClick={this.reset}>C</Button> <Button onClick={this.add('0')}>0</Button> <Button onClick={this.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button onClick={this.calculate}>=</Button> <Button onClick={this.add('+')}>+</Button> <Button onClick={this.add('-')}>-</Button> <Button onClick={this.add('*')}>*</Button> </Grid.Column> </Grid> ); }
  36. <Grid size="4"> <Grid.Column size="3"> <Memory> {this.state.memory.trim()} </Memory> <Button onClick={this.add('1')}>1</Button> <Button

    onClick={this.add('2')}>2</Button> <Button onClick={this.add('3')}>3</Button> <Button onClick={this.add('4')}>4</Button> <Button onClick={this.add('5')}>5</Button> <Button onClick={this.add('6')}>6</Button> <Button onClick={this.add('7')}>7</Button> <Button onClick={this.add('8')}>8</Button> <Button onClick={this.add('9')}>9</Button> <Button onClick={this.reset}>C</Button> <Button onClick={this.add('0')}>0</Button> <Button onClick={this.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button onClick={this.calculate}>=</Button> <Button onClick={this.add('+')}>+</Button> <Button onClick={this.add('-')}>-</Button> <Button onClick={this.add('*')}>*</Button> </Grid.Column> </Grid>
  37. import React from 'react'; import Component from './index'; import {

    shallow, mount } from 'enzyme'; import Memory from './Memory'; describe(Component.name, () => { it('renders memory', () => { const component = shallow(<Component />); expect(component.contains(<Memory>0</Memory>)).toEqual(true); }); });
  38. <Grid size="4"> <Grid.Column size="3"> <Memory> {this.state.memory.trim()} </Memory> <Button onClick={this.add('1')}>1</Button> <Button

    onClick={this.add('2')}>2</Button> <Button onClick={this.add('3')}>3</Button> <Button onClick={this.add('4')}>4</Button> <Button onClick={this.add('5')}>5</Button> <Button onClick={this.add('6')}>6</Button> <Button onClick={this.add('7')}>7</Button> <Button onClick={this.add('8')}>8</Button> <Button onClick={this.add('9')}>9</Button> <Button onClick={this.reset}>C</Button> <Button onClick={this.add('0')}>0</Button> <Button onClick={this.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button onClick={this.calculate}>=</Button> <Button onClick={this.add('+')}>+</Button> <Button onClick={this.add('-')}>-</Button> <Button onClick={this.add('*')}>*</Button> </Grid.Column> </Grid>
  39. <Grid size="4"> <Grid.Column size="3"> <Memory> {this.state.memory.trim()} </Memory> <Button data-test="1" onClick={this.add('1')}>1</Button>

    <Button data-test="2" onClick={this.add('2')}>2</Button> <Button data-test="3" onClick={this.add('3')}>3</Button> <Button data-test="4" onClick={this.add('4')}>4</Button> <Button data-test="5" onClick={this.add('5')}>5</Button> <Button data-test="6" onClick={this.add('6')}>6</Button> <Button data-test="7" onClick={this.add('7')}>7</Button> <Button data-test="8" onClick={this.add('8')}>8</Button> <Button data-test="9" onClick={this.add('9')}>9</Button> <Button data-test="reset" onClick={this.reset}>C</Button> <Button data-test="0" onClick={this.add('0')}>0</Button> <Button data-test="remove" onClick={this.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button data-test="calculate" onClick={this.calculate}>=</Button> <Button data-test="+" onClick={this.add('+')}>+</Button> <Button data-test="-" onClick={this.add('-')}>-</Button> <Button data-test="*" onClick={this.add('*')}>*</Button> </Grid.Column> </Grid>
  40. it('can add digits and operands', () => { const component

    = mount(<Component />); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="+"]').simulate('click'); component.find('[data-test="1"]').simulate('click'); expect(component.contains(<Memory>1 + 1</Memory>)).toEqual(true); }); it('can sum numbers', () => { const component = mount(<Component />); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="+"]').simulate('click'); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="calculate"]').simulate('click'); expect(component.contains(<Memory>2</Memory>)).toEqual(true); }); it('can remove digits and operands', () => { const component = mount(<Component />); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="+"]').simulate('click'); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="remove"]').simulate('click'); expect(component.contains(<Memory>1 +</Memory>)).toEqual(true); });
  41. it('can add digits and operands', () => { const component

    = mount(<Component />); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="+"]').simulate('click'); component.find('[data-test="1"]').simulate('click'); expect(component).toContainReact(<Memory>1 + 1</Memory>); }); it('can sum numbers', () => { const component = mount(<Component />); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="+"]').simulate('click'); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="calculate"]').simulate('click'); expect(component).toContainReact(<Memory>2</Memory>); }); it('can remove digits and operands', () => { const component = mount(<Component />); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="+"]').simulate('click'); component.find('[data-test="1"]').simulate('click'); component.find('[data-test="remove"]').simulate('click'); expect(component).toContainReact(<Memory>1 +</Memory>); });
  42. Used as a simpler implementation, e.g. using an in-memory database

    in the tests instead of doing real database access. Fake
  43. Used when a parameter is needed for the tested method

    but without actually needing to use the parameter. Dummy object
  44. Mocks are fake methods (like spies) with pre- programmed behaviour

    (like stubs) as well as pre- programmed expectations. A mock will fail your test if it is not used as expected. Mock
  45. import { connect } from 'react-redux'; import { setNotification }

    from 'modules/notification'; export class NotificationButton extends React.Component { render() { return <button onClick={this.handleClick}>Trigger Alert</button>; } handleClick = () => { this.props.setNotification(this.props.notification); }; } export default connect(null, { setNotification })(NotificationButton);
  46. describe(Component.name, () => { it('triggers `setNotification` onClick', () => {

    const spy = sinon.spy(); const component = shallow( <Component setNotification={spy} notification="test" />, ); component.find('button').simulate('click'); expect(spy).toHaveBeenCalledWith('test'); }); });
  47. $ https://github.com/facebook/jest " https://github.com/jasmine/jasmine ! https://github.com/mochajs/mocha & https://github.com/chaijs/chai # https://github.com/sinonjs/sinon

    ' https://github.com/airbnb/enzyme/ ( https://github.com/producthunt/chai-enzyme % https://github.com/graphcool/chromeless Tools