Unit Testing Workshop

 

The Problem

Testing applications by hand is hard and repetitive.

It's fine for smaller projects.

But at some point you'll build an application that's too big for you to be able to test everything by hand.

Introducing

Automated

Testing

Automated testing lets us test our application by writing specific test cases that we can keep testing as we develop our application.

We'll be working in Node.js

What is Node.js?

Node is a JavaScript runtime.

What is Node.js?

Normally JavaScript runs in the browser for websites. Node lets you run JavaScript outside of the browser.

Every language and platform will have its own unit testing tools

Java has JUnit, Python has unittest etc.

We're just using Node because JavaScript has really nice testing libraries.

Install Node.js

Head to www.nodejs.org and install version 7.1.0 (the latest version)

What is NPM

NPM stands for Node Package Manager

What is NPM

NPM is basically what manages the libraries and other dependencies that our application will use.

Let's create our project

Head to your command line environment and run:

npm init

You should find

A package.json file

The package.json file defines your Node project. It'll hold your application's name, description, license and most importantly, your dependencies.

Let's start by installing our testing framework

We'll be using Mocha

There are tons of testing frameworks for JavaScript, Mocha is one of the most popular.

// --save-dev means this dependency will be saved in package.json
npm install --save-dev mocha

Now let's install chai

Chai gives the functions needed to check for the results we expect

// --save-dev means this dependency will be saved in package.json
npm install --save-dev chai

Create a main.js file

Set this as your startup file

{
  "name": "hackutd-unit-testing",
  "version": "1.0.0",
  "description": "Unit testing workshop",
  "main": "main.js",
  "scripts": {
    "start": "node main.js",
    "test": "mocha test/ --recursive"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^3.5.0",
    "mocha": "^3.1.2"
  }
}

Let's write our first function

module.exports = {
  reverse(text) {
    return text.split('').reverse().join('');
  },
};

Now let's write some tests

const assert = require('chai').assert;
const utils = require('../utils');

describe('utils', function () {
  describe('reverse', function () {
    it('reverses string', function () {
      const result = utils.reverse('test');
      assert.equal(result, 'tset');
    });
  });
});

Now let's run it

npm test

Unit Testing

Unit testing is about testing "units" of your code.

Testing can be tedious at times, but it can give you peace of mind

Testing won't make your application completely bug-free, it'll just eliminate a lot of them.

const assert = require('chai').assert;
const utils = require('../utils');

describe('utils', function () {
  describe('reverse', function () {
    it('reverses string', function () {
      const result = utils.reverse('test');
      assert.equal(result, 'tset');
    });
    it('handles non-strings', function () {
      const result = utils.reverse(0);
      assert.equal(result, undefined);
    });
  });
});

Run the tests again

module.exports = {
  reverse(text) {
    if (typeof text === 'string') {
        return text.split('').reverse().join('');
    }
    
    return undefined;
  }
};

Now run the tests

They should pass now

module.exports = {
  reverse(text) {
    if (this.isString(text)) {
        return text.split('').reverse().join('');
    }

    return undefined;
  },
  isString(value) {
    return typeof value === 'string';
  }
};

A big part of testing is developing modular "testable" functions

const assert = require('chai').assert;
const utils = require('../utils');

describe('utils', function () {
  describe('reverse', function () {
    it('reverses string', function () {
      const result = utils.reverse('test');
      assert.equal(result, 'tset');
    });
    it('handles non-strings', function () {
      const result = utils.reverse(0);
      assert.equal(result, undefined);
    });
  });
  describe('isString', function () {
    it('returns true for string', function () {
      const result = utils.isString('text');
      assert.equal(result, true);
    });
    it('returns false for non-strings', function () {
      const result = utils.isString(0);
      assert.equal(result, false);
    });
  });
});

Run your tests again

They should pass

N​ow let's write another function

module.exports = {
  reverse(text) {
    if (this.isString(text)) {
      return text.split('').reverse().join('');
    }

    return undefined;    
  },
  isString(value) {
    return typeof value === 'string';
  },
  piglatin(message) {
    const words = message.split(' ');
    
    for (let i = 0; i < words.length; i++) {
      words[i] = words[i].reverse();
      words[i] = words[i] + 'ay'
    }

    const result = words.join(' ');

    return result;
  },
};
const assert = require('chai').assert;
const utils = require('../utils');

describe('utils', function () {
  describe('reverse', function () {
    it('reverses string', function () {
      const result = utils.reverse('test');
      assert.equal(result, 'tset');
    });
    it('handles non-strings', function () {
      const result = utils.reverse(0);
      assert.equal(result, undefined);
    });
  });
  describe('isString', function () {
    it('returns true for string', function () {
      const result = utils.isString('text');
      assert.equal(result, true);
    });
    it('returns false for non-strings', function () {
      const result = utils.isString(0);
      assert.equal(result, false);
    });
  });
  describe('piglatin', function () {
    it('converts sentence to piglatin', function () {
      const result = utils.piglatin('Hello my name is');
      assert.equal(result, 'Ollehay ymay emanay siay');
    });
  });
});

Run your tests

The piglatin test shouldn't pass

module.exports = {
  reverse(text) {
    if (this.isString(text)) {
      return text.split('').reverse().join('');
    }

    return undefined;    
  },  
  isString(value) {
    return typeof value === 'string';
  },
  piglatin(message) {
    const words = message.toLowerCase().split(' ');
    
    for (let i = 0; i < words.length; i++) {
      words[i] = this.reverse(words[i]);
      words[i] = words[i] + 'ay';
    }

    const result = words.join(' ');
    const sentence = result.charAt(0).toUpperCase() + result.slice(1);

    return sentence;
  },
};

Try running them again

They should pass now

const assert = require('chai').assert;
const utils = require('../utils');

describe('utils', function () {
  describe('reverse', function () {
    it('reverses string', function () {
      const result = utils.reverse('test');
      assert.equal(result, 'tset');
    });
    it('handles non-strings', function () {
      const result = utils.reverse(0);
      assert.equal(result, undefined);
    });
  });
  describe('isString', function () {
    it('returns true for string', function () {
      const result = utils.isString('text');
      assert.equal(result, true);
    });
    it('returns false for non-strings', function () {
      const result = utils.isString(0);
      assert.equal(result, false);
    });
  });
  describe('piglatin', function () {
    it('converts sentence to piglatin', function () {
      const result = utils.piglatin('Hello my name is');
      assert.equal(result, 'Ollehay ymay emanay siay');
    });
    it('converts word to piglatin', function () {
      const result = utils.piglatin('Hello');
      assert.equal(result, 'Ollehay');
    });
    it('handles non-strings', function () {
      const result = utils.piglatin(0);
      assert.equal(result, '');
    });
  });
});

Run your tests

One of them will fail

module.exports = {
  reverse(text) {
    if (this.isString(text)) {
      return text.split('').reverse().join('');
    }

    return undefined;    
  },  
  isString(value) {
    return typeof value === 'string';
  },
  piglatin(message) {
    if (this.isString(message)) {
      const words = message.toLowerCase().split(' ');
    
      for (let i = 0; i < words.length; i++) {
        words[i] = this.reverse(words[i]);
        words[i] = words[i] + 'ay';
      }

      const result = words.join(' ');
      const sentence = result.charAt(0).toUpperCase() + result.slice(1);

      return sentence;
    }
    
    return undefined;
  },
};

And all of them should pass

Finally let's setup our main file

const utils = require('./utils');
const readline = require('readline');

const interface = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

interface.question('Enter a word: ', function (word) {
  console.log('Reversed:', utils.reverse(word));

  interface.question('Enter a sentence: ', function (sentence) {
    console.log('Piglatin:', utils.piglatin(sentence));

    process.exit();
  });
});

Acceptance testing

Acceptance testing

Unit testing is about testing independent units of your application.

 

​Acceptance testing is the complete opposite. It tests user interaction and application flow. So if you are building a website, you would simulate a user using your application (clicking on buttons, filling out forms) in your tests.

describe('posts page', function () {
  it('should add new post', function() {
    visit('/posts/new');
    fillIn('input.title', 'My new post');
    click('button.submit');
    andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post'));
  });
});

Integration tests

Integration testing

Integration testing is a middle ground between acceptance testing and unit testing.

 

Integration testing tests interactions between parts of your application. It doesn't test user flow like acceptance tests but on a website, for example, it could test how different UI components interact with each other.

Regression testing

Regression testing

A​ test you add once you fix a bug to ensure that bug doesn't happen again.

You don't have to choose just one

(And shouldn't)

Each type of testing works better for testing certain types of features.

Writing your test cases first and then writing your function to pass those tests. You know exactly what your function should do and what cases it should support.

Test-driven development (TDD)

Continuous Integration

Fits in with your code repository (GitHub, BitBucket, GitLab etc)

Runs your tests each time you commit or pull request

So if you commit code that breaks something, you'll know.

G​ives you more peace of mind when collaborating

You'll know when you break something, but you'll also know if someone else breaks something.

T​ravis CI

Free for public GitHub repos. It's normally paid for private GitHub repos but students get it free with the GitHub Student Developer Pack.

We're going to look at the continuous integration page for a popular open-source library. Because this is an open-source project, the continuous integration page is also completely public.

HackUTD Unit Testing Workshop

By Bharat Arimilli

HackUTD Unit Testing Workshop

  • 687