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

Unit Testing im Frontend - Lightning Talk

Unit Testing im Frontend - Lightning Talk

Slides to my (german) Talk at the Cosee TechTalks.

Mirjam Aulbach

November 29, 2018
Tweet

More Decks by Mirjam Aulbach

Other Decks in Programming

Transcript

  1. UNIT TESTING
    IM FRONTEND
    Klingt komisch. Is aber so.
    MIRJAM BÄUERLEIN | @MIRJAM_DIALA

    View Slide

  2. I
    !
    Testing

    View Slide

  3. WER TESTET SEINEN CODE?

    View Slide

  4. WER TESTET SEINEN CODE IM FRONTEND?

    View Slide

  5. Browser neuladen zählt dazu...

    View Slide

  6. Was ist eigentlich...
    AUTOMATISIERTES SOFTWARE TESTING?

    View Slide

  7. UNIT TESTS

    View Slide

  8. INTEGRATION TESTS

    View Slide

  9. END TO END TESTS

    View Slide

  10. END TO END
    INTEGRATION
    UNIT TESTS

    View Slide

  11. Muss ich testen?

    View Slide

  12. View Slide

  13. "DOES YOUR TEAM WRITE TESTS FOR BACK-END CODE?"1
    1 State Of The Web Survey, August 2018, https://dev.to/devteam/state-of-the-web-data---call-for-analysis-2o75

    View Slide

  14. "DOES YOUR TEAM WRITE TESTS FOR FRONT-END CODE?"1
    1 State Of The Web Survey, August 2018, https://dev.to/devteam/state-of-the-web-data---call-for-analysis-2o75

    View Slide

  15. ACTUAL FOOTAGE OF ME SEEING THIS RESULT

    View Slide

  16. Historisch gewachsen
    FRONTEND !== FRONTEND
    ANYMORE

    View Slide

  17. View Slide

  18. View Slide

  19. View Slide

  20. But how?
    UNIT TESTING IM FRONTEND

    View Slide

  21. Beispiel 1
    FORMATTER
    VANILLA JS

    View Slide

  22. document.addEventListener("DOMContentLoaded", function() {
    var unformatedSum = document.querySelector(".js-sum").innerHTML;
    var sumNum = unformatedSum.toString().split('').reverse();
    var sumFormatted = [];
    for (var i = 0; i < sumNum.length; i++) {
    var currNum = sumNum[i];
    if (i != 0 && i % 3 == 0) {
    sumFormatted.push('.');
    }
    sumFormatted.push(currNum);
    }
    if (sumFormatted.length > 0) {
    sumFormatted.reverse();
    sumFormatted.push(' Euro');
    }
    var formatedSum = sumFormatted.join('');
    document.querySelector(".js-sum").innerHTML = formatedSum;
    });

    View Slide

  23. document.addEventListener("DOMContentLoaded", function() {
    var sumElement = document.querySelector(".js-sum")
    var unformatedSum = sumElement.innerHTML;
    var formatedSum = moneyFormatter(unformatedSum)
    sumElement.innerHTML = formatedSum;
    });

    View Slide

  24. function moneyFormatter(sum) {
    var sumNum = sum.toString().split('').reverse();
    var sumFormatted = [];
    for (var i = 0; i < sumNum.length; i++) {
    var currNum = sumNum[i];
    if (i != 0 && i % 3 == 0) {
    sumFormatted.push('.');
    }
    sumFormatted.push(currNum);
    }
    if (sumFormatted.length > 0) {
    sumFormatted.reverse();
    sumFormatted.push(' Euro');
    }
    return sumFormatted.join('')
    }

    View Slide

  25. function assertEqual(actual, expected) {
    if (expected === actual) {
    console.info('[SUCCESS] Is ' + expected);
    } else {
    console.error('[ERROR] Expected ' + actual + ' to be ' + expected);
    }
    }
    function tests() {
    assertEqual(formatSum(1), '1 Euro');
    assertEqual(formatSum(12), '12 Euro');
    assertEqual(formatSum(123), '123 Euro');
    assertEqual(formatSum(1234), '1.234 Euro');
    assertEqual(formatSum(12345), '12.345 Euro');
    assertEqual(formatSum(123456), '123.456 Euro');
    assertEqual(formatSum(1234567), '1.234.567 Euro');
    }

    View Slide

  26. Beispiel 2
    SUBMIT BUTTON
    JQUERY, ES6

    View Slide

  27. function registerFormSubmit() {
    $('body').on('submit', handleFormSubmit);
    }
    function handleFormSubmit(event) {
    const submittedButton = $(event.target).find('[type="submit"]');
    submittedButton.addClass('disabled');
    formProcessingAnimation(submittedButton);
    }
    function appendLoadingDots(element) {
    const loadingDotHtml = '.';
    element.append(loadingDotHtml).append(loadingDotHtml).append(loadingDotHtml);
    }
    function formProcessingAnimation(button) {
    button.addClass('processing');
    button.text('Processing');
    appendLoadingDots(button);
    }
    export default { registerFormSubmit };

    View Slide

  28. describe('register form submit', function() {
    const registerFormSubmit = formHelpers.registerFormSubmit;
    let testForm = null;
    beforeEach(function() {
    testForm = $('').append('Hello');
    $('body').append(testForm);
    registerFormSubmit();
    });
    afterEach(function() {
    testForm.remove();
    });
    it('defines a registerFormSubmit function', function() {
    expect(typeof registerFormSubmit).toBe('function');
    });
    it('disables the button on submit', function() {
    expect(testForm.find('button')).not.toHaveClass('disabled');
    testForm.submit();
    expect(testForm.find('button')).toHaveClass('disabled');
    });
    it('adds class "processing" on submit', function() {
    expect(testForm.find('button')).not.toHaveClass('processing');
    testForm.submit();
    expect(testForm.find('button')).toHaveClass('processing');
    });
    it('replaces the button text with "Processing"', function() {
    expect(testForm.find('button')).toContainText('Hello');
    testForm.submit();
    expect(testForm.find('button')).toContainText('Processing...');
    });
    it('adds three dots on submit', function() {
    expect(testForm.find('button').find('.loading-dot')).toHaveLength(0);
    testForm.submit();
    expect(testForm.find('button').find('.loading-dot')).toHaveLength(3);
    });
    });

    View Slide

  29. Beispiel 3
    LINK COMPONENT
    REACTJS

    View Slide

  30. const ButtonExternalLink = ({ url, children, primary, secondary, small }) => {
    const btnCustomClass = small ? 'btn-custom-small' : 'btn-custom'
    const primaryClass = primary ? ' btn-primary' : ''
    const secondaryClass = secondary ? ' btn-secondary' : ''
    return (
    className={`btn ${btnCustomClass}${primaryClass}${secondaryClass}`}
    href={url}
    >
    {children}

    )
    }
    export default ButtonExternalLink

    View Slide

  31. const testUrl = 'http://forum.conferencebuddy.io/'
    const linkText = 'Test Button Linktext'
    const linkImage =
    describe('', () => {
    it('renders without errors', () => {
    expect(() =>
    shallow(

    )
    ).not.toThrow()
    })
    it('renders without console errors', () => {
    expect(() =>
    shallow(
    {linkText}
    )
    ).not.toConsoleError()
    })
    })

    View Slide

  32. describe('renders a link with an url and the "btn" class', () => {
    const wrapper = shallow(
    {linkText}
    )
    it('renders a link element', () => {
    expect(wrapper.find('a')).toHaveLength(1)
    })
    it('renders a link with a given url', () => {
    expect(wrapper.find('a').prop('href')).toEqual(testUrl)
    })
    it('renders a link with a class', () => {
    expect(wrapper.hasClass('btn')).toBe(true)
    })
    })
    describe('renders a link with a given content', () => {
    it('renders a link with a text as children', () => {
    const wrapper = shallow(
    {linkText}
    )
    expect(wrapper.text()).toBe(linkText)
    })
    it('renders a link with an image as children', () => {
    const wrapper = shallow(
    {linkImage}
    )
    expect(wrapper.contains(linkImage)).toBe(true)
    })
    })
    describe('can style specific looks "primary" and "secondary"', () => {
    it('adds a class for style primary', () => {
    const wrapper = shallow(

    {linkText}

    )
    expect(wrapper.hasClass('btn-primary')).toBe(true)
    expect(wrapper.hasClass('btn-secondary')).toBe(false)
    })
    it('adds a class for style secondary', () => {
    const wrapper = shallow(

    {linkText}

    )
    expect(wrapper.hasClass('btn-secondary')).toBe(true)
    expect(wrapper.hasClass('btn-primary')).toBe(false)
    })
    })

    View Slide

  33. TESTS ARE CODE.

    View Slide

  34. DANKE!
    !
    [email protected]
    "
    @mirjam_diala
    #
    programmiri

    View Slide