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