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. Testing in JavaScript
    Radoslav Stankov 27/08/2017

    View Slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View Slide

  3. View Slide

  4. View Slide

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

    View Slide

  6. Why do automated testing?

    View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  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

    View Slide

  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

    View Slide

  17. View Slide

  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

    View Slide

  19. Terminology

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  23. View Slide

  24. “I don’t have to think how to run my tests.“
    Easy to run

    View Slide

  25. “Running tests, should not distract me from the
    problem I’m solving.“
    Quick

    View Slide

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

    View Slide

  27. “I should have confidence, that when my test
    pass, my code is most probably is working.”
    Reliable

    View Slide

  28. “I should not rewrite my tests, every time I
    change something.”
    Flexible

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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);
    });
    });
    });

    View Slide

  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

    View Slide

  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

    View Slide

  40. Four Phase Test

    View Slide

  41. Four Phase Test
    Setup

    View Slide

  42. Four Phase Test
    Setup
    Action

    View Slide

  43. Four Phase Test
    Setup
    Action
    Assertion

    View Slide

  44. Four Phase Test
    Setup
    Action
    Assertion
    Teardown

    View Slide

  45. Four Phase Test
    Setup
    Action
    Assertion
    Teardown

    View Slide

  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);
    });
    });
    });

    View Slide

  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);
    });
    });
    });

    View Slide

  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');
    });
    });
    });

    View Slide

  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');
    });
    });
    });

    View Slide

  50. Automated
    Testing
    Test Driven
    Development

    View Slide

  51. Test Driven Development

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  56. View Slide

  57. View Slide


  58. calculate('1 + 1') Ⱦ'2'
    calculate('2 - 1') Ⱦ'1'
    calculate('2 * 2') Ⱦ'4'

    View Slide

  59. describe(calculate.name, () => {

    it('handles "1 + 1"');
    it('handles "2 - 1"');
    it('handles "2 * 2"');
    });

    View Slide

  60. Code Test

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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');

    View Slide

  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');
    });
    });
    });

    View Slide

  71. Testing React

    View Slide

  72. View Slide

  73. export default class Calculator extends React.Component {
    state = {
    memory: '0',
    };
    render() {
    return (



    {this.state.memory.trim()}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    C
    0



    =
    +
    -
    *

    View Slide

  74. 2
    3
    4
    5
    6
    7
    8
    9
    C
    0



    =
    +
    -
    *


    );
    }
    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 });
    }

    View Slide

  75. };
    render() {
    return (



    {this.state.memory.trim()}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    C
    0



    =
    +
    -
    *


    );
    }

    View Slide




  76. {this.state.memory.trim()}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    C
    0



    =
    +
    -
    *


    View Slide

  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();
    expect(component.contains(0)).toEqual(true);
    });
    });

    View Slide




  78. {this.state.memory.trim()}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    C
    0



    =
    +
    -
    *


    View Slide




  79. {this.state.memory.trim()}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    C
    0



    =
    +
    -
    *


    View Slide

  80. it('can add digits and operands', () => {
    const component = mount();
    component.find('[data-test="1"]').simulate('click');
    component.find('[data-test="+"]').simulate('click');
    component.find('[data-test="1"]').simulate('click');
    expect(component.contains(1 + 1)).toEqual(true);
    });
    it('can sum numbers', () => {
    const component = mount();
    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(2)).toEqual(true);
    });
    it('can remove digits and operands', () => {
    const component = mount();
    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(1 +)).toEqual(true);
    });

    View Slide

  81. View Slide

  82. it('can add digits and operands', () => {
    const component = mount();
    component.find('[data-test="1"]').simulate('click');
    component.find('[data-test="+"]').simulate('click');
    component.find('[data-test="1"]').simulate('click');
    expect(component).toContainReact(1 + 1);
    });
    it('can sum numbers', () => {
    const component = mount();
    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(2);
    });
    it('can remove digits and operands', () => {
    const component = mount();
    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(1 +);
    });

    View Slide

  83. View Slide

  84. View Slide

  85. Mocks

    View Slide

  86. View Slide

  87. Used as a simpler implementation, e.g. using
    an in-memory database in the tests instead of
    doing real database access.
    Fake

    View Slide

  88. Used when a parameter is needed for the
    tested method but without actually needing to
    use the parameter.
    Dummy object

    View Slide

  89. Test stubs are objects with pre-programmed
    behaviour.
    Stub

    View Slide

  90. Records arguments, return value, the value of
    this and exception thrown (if any) for all its calls.
    Spy

    View Slide

  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

    View Slide

  92. Test double
    Stub
    Spy
    Mock
    Fake
    Dummy

    View Slide


  93. http://sinonjs.org


    View Slide

  94. import { connect } from 'react-redux';
    import { setNotification } from 'modules/notification';
    export class NotificationButton extends React.Component {
    render() {
    return Trigger Alert;
    }
    handleClick = () => {
    this.props.setNotification(this.props.notification);
    };
    }
    export default connect(null, { setNotification })(NotificationButton);

    View Slide

  95. describe(Component.name, () => {
    it('triggers `setNotification` onClick', () => {
    const spy = sinon.spy();
    const component = shallow(
    ,
    );
    component.find('button').simulate('click');
    expect(spy).toHaveBeenCalledWith('test');
    });
    });

    View Slide

  96. sinon.useFakeTimers()
    sinon.useFakeXMLHttpRequest()
    sinon.stub()
    sinon.spy()
    sinon.mock(myAPI)

    View Slide

  97. Tools

    View Slide

  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

    View Slide

  99. Books

    View Slide

  100. View Slide

  101. View Slide

  102. View Slide

  103. Thanks )

    View Slide

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

    View Slide

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

    View Slide