Unit Tests

Rigor

"the process of being thorough, accurate, or exhaustive"

Professionalism

Profession-alism

 

Doing the right thing by your profession, rather than the right thing for yourself

Rigor: the process of being thorough, accurate, or exhaustive

Pair

Programming

Code
Reviews

Unit

Tests

Why write unit tests?

Projects that include unit tests:

  • Safer to build and deploy
  • Easier to refactor
  • Faster to learn
  • Self-documenting API

Projects that include unit tests:

  • deceptive sense of safety
    • security

Can also be

It's good to have a sense of safety, but not a deceptive sense of safety.

What is a "unit"?

  • A Function
  • Single purpose
  • 1 input => 1 output
let unit = (input) => {
    //...
    return output;
};

What is a "test"?

 

Call a function with mock data and observe it's output

 

Like other forms of testing, unit tests measures success against criteria

Writing "units"

  • Functions become
    • smaller
    • reusable
    • single-purpose

Functions become... functional

pure functions

integrations

const add = (a, b) => {
   return a + b;
};

const myAlgo = (a, b) => {
   //...
   return result;
}
const pageLoad = () => {
   A.init();
   B.init();
   // do all the things
};

const $ = () => {
   return {
      //all the stuff
   }
}

dirty

A unit test should have the following properties:

  • It should be automated and repeatable.
  • It should be easy to implement.
  • Once it’s written, it should remain for future use.
  • Anyone should be able to run it.
  • It should run at the push of a button.
  • It should run quickly.

To borrow from mathematics

When functions are unknown, they can be understood and approximated by modeling input to output values

Given f(x) = y
Givenf(x)=yGiven f(x) = y
f(0) = 0
f(0)=0f(0) = 0
f(2) = 4
f(2)=4f(2) = 4
f(3) = 9
f(3)=9f(3) = 9
f(x) = 2x
f(x)=2xf(x) = 2x
f(4) = 16
f(4)=16f(4) = 16
f(x) = x^2
f(x)=x2f(x) = x^2

Requirement

Iterative

Development

Test

Driven

Development

Basics of testing

import cookieToObject from '../cookieToObject.js';

const getUserSettings = (cookie) => {
    const data = cookieToObject(cookie);
    return {
        autoPlay: data.autoPlay ? data.autoPlay : false,
        profileColor: data.profileColor ? data.profileColor : '#fff'
    };
};
  • describing the `getUserSettings` function
    • it should
      • return an object with data that matches their settings
      • return defaults if they don't have settings

1. Examine Function

2. Describe using natural language

Basics of testing

describe('getUserSettings', () => {

    it('returns default data if nothing is passed in', () => {
        data = getUserSettings();
        assert.false(data.autoPlay);
        assert.equal(data.profileColor, '#fff');
    });

    if('returns correct settings' => {
        data = getUserSettings('autoPlay=true&profileColor=#00f');
        assert.equal(data.profileColor, '#00f');
        assert.true(data.autoPlay);
    });

});
  • describing the `getUserSettings` function
    • it should
      • return an object with data that matches their settings
      • return defaults if they don't have settings

Testing Technology

In these example

A framework which will execute and report tests, can run phantom.js, chrome,  babel, browserify, etc

Libraries which you write the actual test code in. Supports should and assert syntax

Complex Behavior

Click loads

image flyout

Image caption loaded

from data attribute

Image flyout should be responsive & resized to fit container

Arrows should

navigate

through gallery

"x" should

close flyout

Large image

loaded from

data attribute

All that functionality & complexity boils down to:

DOM manipulation

  • Writing dom code in a way that can be unit tested
  • writing the unit tests themselves

Photo Gallery module example

let gallery = () => {
   document.querySelector('.flyout__close').addEventListener('click', (evt) => {
      let flyout = document.querySelector('.flyout');
      flyout.parentNode.removeChild(flyout);
   });
};

export default {
   gallery
};

Photo Gallery test example

import {assert} from 'chai';
import {gallery} from './index.js';

describe('photoGallery', () => {
   describe('close button', () => {
      it('removes the drawer when clicked', (done) => {
      
      
      });
   });
});
let gallery = () => {
   document.querySelector('.flyout__close').addEventListener('click', (evt) => {
      let flyout = document.querySelector('.flyout');
      flyout.parentNode.removeChild(flyout);
   });
};

export default {
   gallery
};

Refactor photoGallery function

import {assert} from 'chai';
import {gallery} from './index.js';

describe('photoGallery', () => {
   describe('close button', () => {
      it('removes the drawer when clicked', (done) => {
      
      
      });
   });
});
let gallery = ({context = document} = {}) => {
   context.querySelector('.flyout__close').addEventListener('click', (evt) => {
      let flyout = context.querySelector('.flyout');
      flyout.parentNode.removeChild(flyout);
   });
};

export default {
   gallery
};

Pass context to module, default to `document`

Use `context` in place of document

Modify unit test

import {assert} from 'chai';

import {gallery} from './index.js';

let fragment = document.createElement('div');

fragment.innerHTML = `<div class="flyout">
                        <a class="flyout__close" href="#close-flyout">X Close</a>
                      </div>`;

describe('photoGallery', () => {
  
  describe('close button', () => {
    it('removes the drawer when clicked', (done) => {
      
      gallery({context: fragment});

    });
  });
});
let gallery = ({context = document} = {}) => {
   context.querySelector('.flyout__close').addEventListener('click', (evt) => {
      let flyout = context.querySelector('.flyout');
      flyout.parentNode.removeChild(flyout);
   });
};

export default {
   gallery
};

Mock the dom fragment

Pass the fragment

to thegallery function

Modify unit test

import {assert} from 'chai';
import {gallery} from './index.js';
let fragment = document.createElement('div');
fragment.innerHTML = `<div class="flyout">
                        <a class="flyout__close" href="#close-flyout">X Close</a>
                      </div>`;

describe('photoGallery', () => {
  describe('close button', () => {
    it('removes the drawer when clicked', () => {
      
      gallery({context: fragment});

      let closeBtn = fragment.querySelector('.flyout__close');
      
      closeBtn.addEventListener('click', (evt) => {
        assert.isFalse(!!fragment.querySelector('.flyout'));
      })
      
      closeBtn.dispatchEvent(new Event('click'));
      
    });
  });
});
export default function photoGallery({context = document} = {}){
  context.querySelector('.flyout__close').addEventListener('click', (evt) => {
    let flyout = context.querySelector('.flyout');
    flyout.parentNode.removeChild(flyout);
  });
};

Programmatically "click" on the close button

Verify that the `.flyout` is gone after the click

Modify unit test

let fragment document.createElement('div');
fragment.innerHTML = `<div class="flyout">
                    <a class="flyout__close" href="#close-flyout">X Close</a>
                </div>`;

gallery = photoGallery({context: fragment});

describe('photoGallery', () => {
  describe('close button', () => {
    it('removes the drawer when clicked', (done) => {
      closeBtn = fragment.querySelector('.flyout__close');
      
      closeBtn.addEventListener('click', (evt) => {
        assert.false(fragement.querySelector('.flyout'));
        done();
      })
      
      closeBtn.dispatchEvent(new Event('click'));
      
    });
  });
});
let gallery = ({context = document} = {}) => {
   context.querySelector('.flyout__close').addEventListener('click', (evt) => {
      let flyout = context.querySelector('.flyout');
      flyout.parentNode.removeChild(flyout);
   });
};

export default {
   gallery
};

Pass `done` to the test, then call it when done with an asynchronous operation

What have we accomplished?

Taken a "dirty" function, abstracted the DOM, simulated user input, observed results

 

The close function can now be tested automatically, and a build can notify you if it fails

Refactoring Functions to be Unit Testable

Problem: Inaccessible Functions

 

Solution: Expose Api

export default function init = () => {
   inaccessible();
};

let function inaccessible = () => {
   //...
};
let init = () => {
   accessible();
};

let accessible = () => {
   //...
};

export default {
   init,
   accessible
}

Refactoring Functions to be Unit Testable

Problem: Direct `document` access

 

Solution: abstract the dom

let addClass = (selector, class) => {
   [...document.querySelectorAll(selector)].forEach((elem) => {
      elem.classList.add(class);
   });
};
let addClass = ({
   context = document,
   selector,
   class} = {}) => {
   [...context.querySelectorAll(selector)].forEach((elem) => {
      elem.classList.add(class);
   });
};

Refactoring Functions to be Unit Testable

Problem: Functions aren't discrete (anonymous functions)

 

Solution: Extract functionality into units

let myComplexFunction = (data) => {
   //sort data
   data.sort((a,b) => { 
      //... 
   });
   
   //transform data
   data = data.map((d)=>{ 
      //... 
   });
   return data;
};
let customSort = (a,b) => {
   //...
};

let customMap = (elem) => {
   //...
};

let myComplexFunction = (data) => {
   //sort data
   data.sort(customSort);
   
   //transform data
   data = data.map(customMap);
};

Sum Up

  • Rigor
  • Benefits of Unit Tests
  • Pure functions <==> Integration 
  • Writing unit tests for complex behavior
  • Refactoring for Unit Testability

Thanks.

Made with Slides.com