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 initYou 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 mochaNow 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 chaiCreate 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 testUnit 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
Now 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.
Gives you more peace of mind when collaborating
You'll know when you break something, but you'll also know if someone else breaks something.
Travis 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