UNIT TESTING
BY EXAMPLE
Namir Sayed-Ahmad Baraza
What's a UNIT?
a device that has a specified function, especially one forming part of a complex mechanism
a piece of code that has a specified function, especially one forming part of a complex program
Unit
Module
System
We will focus here!
What's an Unit Test?
A unit test is an automated piece of code that invokes a unit of work in the system and then checks a single assumption about the behavior of that unit of work.
A good unit test is:
- Able to be fully automated
- Has full control over all the pieces running
- Can be run in any order if part of many other tests
- Runs in memory (no DB, Network or File access)
- Consistently returns the same result for same input
- Runs fast
- Test a single logical concept in the system
- Readable
- Maintainable
- Trustworthy
So, how to write tests?
GIVEN
WHEN
THEN
some input
I do something with it
I expect ...
Three sentences
- Some output
- Some state changed
- Some function called
A Simple Example!
Our company, Calculator GmbH, needs a brand new feature. A function that allows them to sum 2 numbers!
sum = function sum(a, b) {
return a + b;
}
We are incredible programmers, so we go to our C9 editor, and we write this:
But then ...
The "beloved" project leader reviews the code and say:
- We only want this function to work with numbers! If some of the arguments is not a number, we must throw an exception
- We don't care about negative numbers! If some of the numbers is negative, we have to consider only the value, not the sign
git checkout presentation-unit-testing-sum-step-1
One of your colleages will say:
He shows you how to write the tests. He encourages you to write them before your actual code:
describe('sum', function() {
it('should sum two numbers and give the result', function () {
// Given two numbers
const a = 2;
const b = 4;
// When i call sum with them as arguments
const result = sum(a, b);
// Then the result should be the sum of both
expect(result).toBe(6);
});
it('should throw an error if any of the arguments is not a number', function () {
// Given two input, one number and another one which is not a number
const a = 2;
const b = '4';
// When I call sum with them as arguments
// Then the function should throw an error
expect(() => { sum(a, b); }).toThrow();
});
it('should consider only the value of the inputs not the sign', function () {
// Given two inputs, one positive number and one negative number
const a = 2;
const b = -4;
// When I call sum with them as arguments
const result = sum(a, b);
// Then the result should be the soum of both values ignoring the sign
expect(result).toBe(6);
});
});
git checkout presentation-unit-testing-sum-step-2
Now, you run them and...
jasmine-server-integration: 2 tests failed
CONSOLE (STDERR) sum should throw an error if any of the arguments is not a number
CONSOLE (STDERR) Expected function to throw an exception.
CONSOLE (STDERR) meteor://💻app/tests/unit/sum.spec.js:18:38: Expected function to throw an exception.
CONSOLE (STDERR)
CONSOLE (STDERR) sum should consider only the value of the inputs not the sign
CONSOLE (STDERR) Expected -2 to be 6.
CONSOLE (STDERR) meteor://💻app/tests/unit/sum.spec.js:28:24: Expected -2 to be 6.
./testPackages.sh packages/sum/
Don't panic! We can solve it!
sum = function sum (a, b) {
// Check not numbers
const typeOfA = typeof a;
const typeOfB = typeof b;
// We check the type of the arguments
if (typeOfA !== 'number' || typeOfB !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return Math.abs(a) + Math.abs(b); // We ignore the sign
};
git checkout presentation-unit-testing-sum-step-3
./testPackages.sh packages/sum/
We run our tests again
jasmine-server-integration: 3 tests passed (10ms)
We've written our first succesful tests!
For some time, everything was nice but ...
Project Leader
We must only allow integers!
You
git checkout presentation-unit-testing-sum-step-4
Implement it!
sum = function sum (a, b) {
// We check the type of the arguments
if (!Number.isInteger(a) || !Number.isInteger(b)) {
throw new TypeError('Arguments must be numbers');
}
return Math.abs(a) + Math.abs(b); // We ignore the sign
};
./testPackages.sh packages/sum/
We run our tests again
jasmine-server-integration: 4 tests passed (10ms)
All our tests still pass!
it(`should throw an error if any of the arguments
is not a integer`, function () {
// Given two arguments,
const a = 2; // A integer
const b = 4.2; // And a float
// When i call sum with them as arguments
// Then the result should be the sum of both
expect(() => { sum(a, b); }).toThrow();
});
Add a new test!
For some time, everything was nice but ... (2)
Project Leader
We must log every operation to the Database!
You
A good unit test is
Able to be fully automated
Has full control over all the pieces running
Can be run in any order if part of many other tests
Runs in memory (no DB, Network or File access)
Runs in memory (no DB, Network or File access)
Project Leader
We must log every operation to the Database!
Don't panic! We can solve it!
Doubles to the rescue!
We have some types of test doubles:
- Fakes (Just for passing arguments)
- Stubs (We use in the test)
- Spies (We want to know if they where called)
- Mocks (Mimic the functionality of the unit it doubles)
So, you have to change your tests
// spyOn OperationLog
// We spy because we want to check if the database was called
beforeEach(function () {
spyOn(OperationLog, 'insert'); // OperationLog is the Collection
});
// Add one test
it('should log every operation to the database', function () {
// Given two numbers
const a = 2;
const b = 4;
// When i call sum with them as arguments
const result = sum(a, b);
// Then the OperationLog should be updated
expect(OperationLog.insert).toHaveBeenCalledWith({
operation: 'sum',
args: [a, b],
result
});
});
git checkout presentation-unit-testing-sum-step-5
And you change the code...
sum = function sum (a, b) {
// We check the type of the arguments
if (!Number.isInteger(a) || !Number.isInteger(b)) {
throw new TypeError('Arguments must be numbers');
}
const result = Math.abs(a) + Math.abs(b); // We ignore the sign
OperationLog.insert({
operation: 'sum',
args: [a, b],
result
});
return result;
};
./testPackages.sh packages/sum/
We run our tests again
jasmine-server-integration: 5 tests passed (10ms)
All our tests pass!
CONGRATULATIONS
But....
We need a few more things
- We need a new method, multiply. it will operate both with integers and floats (positive or negative), but the result should be always rounded.
- It should throw an error if any of the arguments is 0
- We want to know when each operation was done. We need a timestamp or something like this.
What have we learnt?
-
Testing helps you to write specifications/requirements before starting to code
-
Testing helps you to make changes in the code without being scared about breaking it
-
Testing allows you to develop features faster, because you don't need to manually test everystep on every reload.
-
Testing can help you to write better code
Thanks for listening
Questions time!
Unit Testing by Example
By Namir Sayed-Ahmad Baraza
Unit Testing by Example
- 871