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

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

August 20, 2017
Tweet

Transcript

  1. Testing in JavaScript Radoslav Stankov 27/08/2017

  2. Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

  3. None
  4. None
  5. https://speakerdeck.com/rstankov/testing-in-javascript

  6. Why do automated testing?

  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. • 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
  16. • 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
  17. None
  18. ! 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
  19. Terminology

  20. System under test (SUT) Tests Unit test Integration test Acceptance

    test SUD
  21. 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
  22. 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
  23. None
  24. “I don’t have to think how to run my tests.“

    Easy to run
  25. “Running tests, should not distract me from the problem I’m

    solving.“ Quick
  26. “I should not spend any energy if understanding the test.“

    Simple
  27. “I should have confidence, that when my test pass, my

    code is most probably is working.” Reliable
  28. “I should not rewrite my tests, every time I change

    something.” Flexible
  29. “When a test fails I should know exactly why?” Localisable

  30. “One test should not depend on other test” Isolated

  31. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

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

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); });
  33. 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
  34. 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
  35. 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
  36. describe(Calculator, () => { describe('evaluate', () => { it('handles "1

    + 1" input', () => { const calculator = new Calculator('1 + 1'); const value = calculator.evaluate(); expect(value).toEqual(2); }); }); }); Assertion
  37. 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); }); }); });
  38. 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
  39. 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
  40. Four Phase Test

  41. Four Phase Test Setup

  42. Four Phase Test Setup Action

  43. Four Phase Test Setup Action Assertion

  44. Four Phase Test Setup Action Assertion Teardown

  45. Four Phase Test Setup Action Assertion Teardown

  46. 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); }); }); });
  47. 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); }); }); });
  48. 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'); }); }); });
  49. 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'); }); }); });
  50. Automated Testing Test Driven Development

  51. Test Driven Development

  52. Test Driven Development 1 Write test ... for code which

    is not written yet
  53. Test Driven Development 1 Write test ... for code which

    is not written yet 2 Write code ... as little as require for the test
  54. 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
  55. 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
  56. None
  57. None
  58. 
 calculate('1 + 1') Ⱦ'2' calculate('2 - 1') Ⱦ'1' calculate('2

    * 2') Ⱦ'4'
  59. describe(calculate.name, () => {
 it('handles "1 + 1"'); it('handles "2

    - 1"'); it('handles "2 * 2"'); });
  60. Code Test

  61. describe(calculate.name, () => { it('handles "1 + 1"'); it('handles "2

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

    { expect(calculate('1 + 1')).toEqual(2); }); it('handles "2 - 1"'); it('handles "2 * 2"'); }); Code Test ✗
  63. 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 ✔
  64. 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 ✗
  65. 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 ✔
  66. 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 ✗
  67. 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 ✔
  68. 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 ✔
  69. 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');
  70. 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'); }); }); });
  71. Testing React

  72. None
  73. 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>
  74. <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 }); }
  75. }; 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> ); }
  76. <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>
  77. 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); }); });
  78. <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>
  79. <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>
  80. 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); });
  81. None
  82. 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>); });
  83. None
  84. None
  85. Mocks

  86. None
  87. Used as a simpler implementation, e.g. using an in-memory database

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

    but without actually needing to use the parameter. Dummy object
  89. Test stubs are objects with pre-programmed behaviour. Stub

  90. Records arguments, return value, the value of this and exception

    thrown (if any) for all its calls. Spy
  91. 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
  92. Test double Stub Spy Mock Fake Dummy

  93. 
 http://sinonjs.org


  94. 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);
  95. 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'); }); });
  96. sinon.useFakeTimers() sinon.useFakeXMLHttpRequest() sinon.stub() sinon.spy() sinon.mock(myAPI)

  97. Tools

  98. $ 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
  99. Books

  100. None
  101. None
  102. None
  103. Thanks )

  104. https://github.com/rstankov/talks-code

  105. https://speakerdeck.com/rstankov/testing-in-javascript