Skip to content

Instantly share code, notes, and snippets.

@bknie1
Last active April 9, 2020 22:08
Show Gist options
  • Select an option

  • Save bknie1/b3a34042f4ffb6827bc30b03908af5fa to your computer and use it in GitHub Desktop.

Select an option

Save bknie1/b3a34042f4ffb6827bc30b03908af5fa to your computer and use it in GitHub Desktop.
Unit Testing and Jasmine

Unit Testing with Jasmine

Table of Contents

Introduction

Getting Started

Essential Keywords

Implementation

Nesting Describe Blocks

Spies

Clocks

Testing Asynchronous Code

Test-Driven Development

Behavior-Driven Development

Test Types

Introduction

TOC Shortcut

Why learn unit testing?

Everyone makes mistakes. As software grows, so does its complexity, and the number of potential issues. We need to test to find those issues. Once we find and understand those mistakes, we can fix them.

What is a unit test?

Unit tests test part of an application which we call units. Very commonly, each unit is tested individually and independently to nesure an application is running as expected. Unit tests should test one piece of functionality. It is okay to have multiple tests for a specific topic but they should be kept separate whenever possible.

Essentials

  1. A test framework
  2. A way of describing the code we are testing
  3. A tool where we can make assertions or expectations about our code

In this guide we are using Jasmine. it works with all kinds of JavaScript environments and Node.js. The syntax is simple and easy to get started with. Mocha and Chai are pretty similar.

Getting Started

TOC Shortcut

First, we need to create our HTML boilerplate and include Jasmine.js and Jasmine.css

<!DOCTYPE html>
    <head>
        <meta charset="utf-8">
        <title>Jasmine Playground</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.5.0/jasmine.css" />
    </head>
    <body>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.5.0/jasmine.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.5.0/jasmine-html.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.5.0/boot.js"></script>
    </body>
</html>

Essential Keywords

TOC Shortcut

These are the essential methods for creating tests with Jasmine.

describe()

Used to organize tests. Used to describe something. This is our jumping off point.

"Let me describe ___ to you."

it()

Used inside the describe() function. We write code that explains, in more detail, what we expect this piece of functionality to do.

"Now that I've described something, let me tell you about 'it' in more detail."

Each 'it' function corresponds to a test. This is also called a spec.

expect()

Lives inside of the 'it' function. We use it to make expectations or assertions about our 'it' functionality. If expectations are not met the test fails.

"Here's what I expect (regarding 'it')."

Matchers

toBe / not.toBe

Uses triple equals to compare a value with another. If used on objects, this means that they must refer to the object same memory reference as well. toEquals is another alternative if you require less specificity.

toBeCloseTo

Will compare two values and accept a second parameter for precision. This is for testing similar items.

toBeDefined

Assuring specific variables have specific values and are not undefined.

toBeFalsey / toBeTruthy

To test a value, when converted to a boolean, to be true/false.

toBeGreaterThan / toBeLessThan

Pretty self explanatory. For working with numbers.

toContain

For working with arrays. To see if a value is contained, or included, in an array.

toEqual

If the two objects are different references in memory triple equals will always be false. toEqual is a less strict version of toBe. If you want to compare the values of two arrays we would want to use toEqual because it allows us to compare objects that have different memory references.

jasmine.any()

A helpful tool for doing type checking. If we want to make sure some value is an array, function, or constructed from a specific function, we can use jasmine.any() to test this.

beforeEach Hook

Runs before each 'it' callback. This is useful for reducing repetitive declarations used in every test in a describe block.

afterEach Hook

Runs after each 'it' callback. Can be used as a teardown to reset counters and other data before subsequent tests begin. This is common when testing databases where we want to start and end with the same data.

beforeAll / afterAll Hook

Runs before or after all tests. They do not reset between tests. This is less common. We could use this to define a variable once that is used in every test.

Implementation

TOC Shortcut

Knowing the essential methods, this is what a Jasmine test may look like:

const earth = {
	isRound: true,
	numberFromSun: 3,
	hasTheWorstCat: false,
	hasTheBestCat: true
};

describe('Earth', () => {
	it('is round', () => {
		expect(earth.isRound).toBe(true);
	});
	it('is the third planet from the sun', () => {
		expect(earth.numberFromSun).toBe(3);
	});
	// it('has the worst cat in the world (Purple)', () => { // TEST FAILURE
	// 	expect(earth.hasTheWorstCat).toBe(true);
	// });
	it('actually has the BEST cat in the world (Purple)', () => {
		expect(earth.hasTheBestCat).toBe(true);
	});
});

describe("Jasmine Matchers", function() {
    it("allows for === and deep equality", function() {
        expect(1+1).toBe(2);
        expect([1,2,3]).toEqual([1,2,3]);
    });
    it("allows for easy precision checking", function() {
        expect(3.1415).toBeCloseTo(3.14,2);
    });
    it("allows for easy truthy / falsey checking", function() {
        expect(0).toBeFalsy();
        expect([]).toBeTruthy();
    });
    it("allows for easy type checking", function() {
        expect([]).toEqual(jasmine.any(Array));
        expect(function(){}).toEqual(jasmine.any(Function));
    });
    it("allows for checking contents of an object", function() {
        expect([1,2,3]).toContain(1);
        expect({name:'Elie', job:'Instructor'}).toEqual(jasmine.objectContaining({name:'Elie'}));
    });
});

Nesting Describe Blocks

TOC Shortcut

We can nest describe blocks to consolidate similar tests. For example, if we wanted to test arrays, we could 'describe' Array and, within that 'describe', continue to 'describe' different tests such as unshift, shift, push, and pop. They may each have their own sub tests in turn as well.

Pending Tests

TOC Shortcut

Used when we do not know what we will be testing or do not want to run a specific test. We can mark a test as pending by:

  1. Omitting a callback function to the 'it' function
  2. Adding a pending() function inside
  3. Adding an 'x' to the 'it' function
describe("Pending specs", function(){
    xit("can start with an xit", function(){
        expect(true).toBe(true);
    });

    it("is a pending test if there is no callback function");

    it("is pending if the pending function is invoked inside the callback", function(){
        expect(2).toBe(2);
        pending();
    });
});

Spies

TOC Shortcut

When unit testing we strive to isolate specific functionality and how this functionality behaves under a variety of circumstances. However, functionality often depends on other parts of the application to work. This is when we use mocking.

A mock is sa fake object that poses as a function without having to go through the overhead of creating a real object. The mock object takes the place of the real object. Mocks can be used to retrieve certain values, how many times the mock function was called, what value the function returned, and how many parameters the function returned.

In Jasmine, mocks are called spies. These are test double functions. A spy can stub / mimic any function and track calls to it and all arguments. Spies only exist in the describe or it block in which it is defined. Spies are removed after each spec.

Creating a Spy

Consider this function:

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

But on what object is it attached? Variables in the global scope are attached to the window object.

Here we spy on the add function and test to see if it has been called. We aren't interested in any return values; we just want to make sure our function has been called.

describe("add", () => {
    let addSpy, result
    beforeEach( () => {
        addSpy = spyOn(window, "add")
        result = addSpy()
    })
    it("is can have params test", () => {
        expect(addSpy).toHaveBeenCalled()
    })
})

toHaveBeenCalled

We can also test functions with parameters using the toHaveBeenCalled() method.

describe("add", () => {
    let addSpy, result
    beforeEach( () => {
        addSpy = spyOn(window, "add")
        result = addSpy()
    })
    it("is can have params test", () => {
        expect(addSpy).toHaveBeenCalled(1, 2, 3)
    })
})

and.callThrough()

We can also see if our spy returns the correct value using the and.callThrough() value. We do this because the original function may take awhile to run or rely on other functions that we cannot access.

describe("add", () => {
    let addSpy, result
    beforeEach( () => {
        addSpy = spyOn(window, "add").and.callThrough()
        result = addSpy()
    })
    it("is can have params test", () => {
        expect(result).toEqual(6)
    })
})

someSpyObject.calls

In our expect block, we can also test call frequency:

    it("is can have params test", () => {
        expect(addSpy.calls.any()).toBe(true)
        expect(addSpy.calls.count()).toBe(5)
    })

Clock

TOC Shortcut

We can use clocks to create timing expectations for our tests.

describe("a simple setTimeout", () => {
    let sample
    beforeEach( () => {
        sample = jasmine.createSpy("sample function")
        jasmine.clock().install()
    })
    afterEach( () => {
        jasmine.clock().uninstall()
    })
})

Testing Asynchronous Code

TOC Shortcut

Jasmine has support for running specs that requires testing asynchronous code. We just have to pass a parameter to any before/after methods or the callback to 'it'. It is commonly called done.

A test will not complete until it's 'done' is called. We can change this, but it is not commonly done.

const getUserInfo = (username) => {
	return $.getJSON('https://api.github.com/users/' + username);
};

describe('#getUserInfo', () => {
	it('returns the correct name for the user', (done) => {
		getUserInfo('bknie1').then((data) => {
			expect(data.name).toBe('Brandon Knieriem');
			done();
			// done() signals that we have completed our async request
			// and the test is allowed to complete
		});
	});
});

Test-Driven Development

TOC Shortcut

In Test-Driven Development (TDD) you write your tests before you write your application code. This means you outline expected outcomes before you even begin solving challenges. We follow a Red, Green, Refactor pattern:

  1. Write the tests
  2. See the tests fail - Red
  3. Write the code necessary to pass the tests - Green
  4. Refactor until all tests are green
  5. Repeat as needed

Even though the process may take a bit longer to produce code it does tend to produce better, more sustainable, bug free code.

Behavior-Driven Development

TOC Shortcut

Jasmine is Behavior-Driven Development (BDD). It is a subset of Test-Driven Development (TDD); you can have BDD without TDD and vice versa. When writing tests, we describe the behavior of our functionality, and not just what we expect the result to be.

The key component to BDD is being verbose with our style and describing the behavior of the functionality. It is helpful when testing the design of the software. Instead of just testing the expected output we describe what is happening to lead up to that output.

Test Types

TOC Shortcut

Unit Tests

Meant to test a component or unit of our application. Unit tests are written to prove that units of our software work. However, these tests may fail when units are combined. This gives way to integration testing.

Jasmine allows us to write unit tests. nUnit is another example of this.

Integration Tests

We test more than one unit and how they function together. It builds off of unit testing. Despite the fact unit tests are the most commonly written test type it is important to also create tests that make sure these units of functionality perform together.

Selenium Web Driver may be an example of this. It simulates user actions to carry out some task which may need to include or evaluate different functionality to reach some expected result.

Acceptance Tests

This involves performing tests on the full system. It evaluates the entire business or system requirements. It is a culmination of all of the functionality in these previous tests.

Stress Tests

Once we have determined that our software meets entire business or systems requirements we need to evaluate and optimize loads. Just because tests pass with 2-3 simulated users does not mean they will pass when 2000-3000 users are trying to perform the same tasks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment