As JavaScript applications grow in complexity, testing becomes not just important—it becomes essential. Whether you're building a single-page application with Backbone.js, adding interactive features to your Rails app, or creating a Node.js server, you need confidence that your code works correctly. That's where Jasmine comes in.
Jasmine is a behavior-driven development (BDD) framework for testing JavaScript code. Unlike traditional unit testing frameworks, Jasmine focuses on describing the behavior of your code in a way that's readable and expressive. You write tests that read almost like English sentences, making it easier to understand what your code is supposed to do.
In this post, I'll walk you through everything you need to know to start testing JavaScript with Jasmine, from basic concepts to advanced techniques like spies and asynchronous testing.
What Is Jasmine?
Jasmine is a standalone JavaScript testing framework created by Pivotal Labs. It doesn't depend on any other JavaScript frameworks or libraries, and it doesn't require a DOM. This makes it perfect for testing JavaScript code in browsers, Node.js, or any other JavaScript environment.
The framework follows BDD principles, which means you describe what your code should do using nested functions called suites and specs. A suite is a group of related tests, and a spec is an individual test case. Together, they form a readable description of your application's behavior.
Jasmine runs in your browser and provides a clean HTML reporter that shows which tests passed and which failed. You can download it from the Pivotal Labs website and get started in minutes.
Setting Up Jasmine
Getting started with Jasmine is straightforward. Download the standalone distribution from the Jasmine website and extract it to your project. You'll get a directory structure that looks like this:
jasmine/
├── lib/
│ └── jasmine-1.3.1/
│ ├── jasmine.css
│ ├── jasmine.js
│ └── jasmine-html.js
├── spec/
│ └── PlayerSpec.js
├── src/
│ └── Player.js
└── SpecRunner.html
The lib directory contains the Jasmine framework files. The src directory is where your application code goes, and spec is where you write your tests. The SpecRunner.html file is an HTML page that loads Jasmine, your source code, and your tests, then runs the tests when you open it in a browser.
Here's what a basic SpecRunner.html looks like:
<!DOCTYPE HTML>
<html>
<head>
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
<!-- include source files here... -->
<script type="text/javascript" src="src/Player.js"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="spec/PlayerSpec.js"></script>
<script type="text/javascript">
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
</script>
</head>
<body>
</body>
</html>
Open this file in your browser, and you'll see the Jasmine test results. Green means passing tests, red means failures.
Writing Your First Test
Let's start with a simple example. Suppose you're building a calculator object with basic arithmetic operations. Here's the code:
// src/Calculator.js
var Calculator = function() {
this.result = 0;
};
Calculator.prototype.add = function(number) {
this.result += number;
};
Calculator.prototype.subtract = function(number) {
this.result -= number;
};
Calculator.prototype.multiply = function(number) {
this.result *= number;
};
Calculator.prototype.divide = function(number) {
if (number === 0) {
throw new Error("Cannot divide by zero");
}
this.result /= number;
};
Calculator.prototype.reset = function() {
this.result = 0;
};
Now let's write tests for this calculator. Create a file called spec/CalculatorSpec.js:
describe("Calculator", function() {
var calculator;
beforeEach(function() {
calculator = new Calculator();
});
it("should start with result equal to 0", function() {
expect(calculator.result).toEqual(0);
});
it("should add numbers correctly", function() {
calculator.add(5);
expect(calculator.result).toEqual(5);
calculator.add(3);
expect(calculator.result).toEqual(8);
});
it("should subtract numbers correctly", function() {
calculator.add(10);
calculator.subtract(4);
expect(calculator.result).toEqual(6);
});
it("should multiply numbers correctly", function() {
calculator.add(5);
calculator.multiply(3);
expect(calculator.result).toEqual(15);
});
it("should divide numbers correctly", function() {
calculator.add(20);
calculator.divide(4);
expect(calculator.result).toEqual(5);
});
it("should throw an error when dividing by zero", function() {
expect(function() {
calculator.divide(0);
}).toThrow(new Error("Cannot divide by zero"));
});
it("should reset the result to 0", function() {
calculator.add(50);
calculator.reset();
expect(calculator.result).toEqual(0);
});
});
Let's break down what's happening here.
Understanding Suites and Specs
The describe function creates a test suite. It takes two parameters: a string describing what you're testing, and a function containing your specs. You can nest describe blocks to organize related tests:
describe("Calculator", function() {
describe("addition", function() {
it("should add positive numbers", function() {
// test code
});
it("should add negative numbers", function() {
// test code
});
});
describe("subtraction", function() {
// more tests
});
});
The it function creates a spec—an individual test. Like describe, it takes a descriptive string and a function. The string should complete the sentence "it should…" This makes your tests read like documentation.
Inside each spec, you use expectations to verify behavior. An expectation is created with the expect function, which takes a value (called the "actual") and chains it with a matcher function that compares it to an expected value.
Working with Matchers
Jasmine comes with a rich set of built-in matchers for common comparisons:
describe("Jasmine Matchers", function() {
it("should demonstrate equality matchers", function() {
var a = 5;
var b = 5;
var c = { name: "John" };
var d = { name: "John" };
expect(a).toEqual(b); // value equality
expect(c).toEqual(d); // deep equality for objects
expect(a).toBe(5); // identity (===)
expect(c).not.toBe(d); // different object references
});
it("should demonstrate truthiness matchers", function() {
var foo = true;
var bar = null;
var baz;
expect(foo).toBeTruthy();
expect(foo).toBe(true);
expect(bar).toBeFalsy();
expect(bar).toBeNull();
expect(baz).toBeUndefined();
expect("hello").toBeDefined();
});
it("should demonstrate comparison matchers", function() {
var pi = 3.14159;
var age = 25;
expect(age).toBeGreaterThan(18);
expect(age).toBeLessThan(100);
expect(pi).toBeCloseTo(3.14, 2); // useful for floating point
});
it("should demonstrate string matchers", function() {
var message = "Hello World";
expect(message).toMatch(/Hello/);
expect(message).toContain("World");
});
it("should demonstrate collection matchers", function() {
var colors = ["red", "green", "blue"];
var empty = [];
expect(colors).toContain("red");
expect(colors.length).toBe(3);
});
it("should demonstrate exception matchers", function() {
var foo = function() {
throw new Error("Something went wrong");
};
expect(foo).toThrow();
expect(foo).toThrow(new Error("Something went wrong"));
});
});
You can also negate any matcher by prefixing it with .not:
expect(5).not.toEqual(10);
expect("hello").not.toMatch(/goodbye/);
Setup and Teardown
Often you need to do some setup before each test or cleanup afterward. Jasmine provides four functions for this:
describe("Setup and Teardown", function() {
var counter;
beforeEach(function() {
counter = 0;
console.log("beforeEach: counter reset to", counter);
});
afterEach(function() {
console.log("afterEach: test completed");
});
it("should increment counter", function() {
counter++;
expect(counter).toEqual(1);
});
it("should start fresh", function() {
expect(counter).toEqual(0); // counter was reset by beforeEach
});
});
The beforeEach function runs before every spec in the suite, and afterEach runs after each spec. There are also beforeAll and afterAll in Jasmine 2.1, but in the current version (1.3), you only have beforeEach and afterEach.
This is perfect for setting up test data, creating objects, or cleaning up after tests. In our Calculator example, we used beforeEach to create a fresh calculator instance for each test:
beforeEach(function() {
calculator = new Calculator();
});
Spies: Testing Interactions
One of Jasmine's most powerful features is spies. A spy is a special function that records how it was called—the arguments, how many times, and what it returned. This is incredibly useful for testing interactions between objects.
Let's say you're building a music player:
// src/MusicPlayer.js
var MusicPlayer = function() {
this.currentTrack = null;
this.isPlaying = false;
};
MusicPlayer.prototype.play = function(track) {
this.currentTrack = track;
this.isPlaying = true;
track.start();
};
MusicPlayer.prototype.pause = function() {
this.isPlaying = false;
if (this.currentTrack) {
this.currentTrack.stop();
}
};
You want to test that when you call play, it calls the track's start method. Here's how to do it with spies:
describe("MusicPlayer", function() {
var player;
var track;
beforeEach(function() {
player = new MusicPlayer();
track = {
title: "Song Title",
start: function() {},
stop: function() {}
};
});
it("should call start on the track when playing", function() {
spyOn(track, 'start');
player.play(track);
expect(track.start).toHaveBeenCalled();
});
it("should call stop on the track when pausing", function() {
spyOn(track, 'stop');
player.play(track);
player.pause();
expect(track.stop).toHaveBeenCalled();
});
it("should mark itself as playing", function() {
spyOn(track, 'start');
expect(player.isPlaying).toBe(false);
player.play(track);
expect(player.isPlaying).toBe(true);
});
});
The spyOn function takes an object and a method name, then replaces that method with a spy. The spy tracks all calls and provides matchers like:
toHaveBeenCalled()– was the spy called at all?toHaveBeenCalledWith(arguments)– was it called with specific arguments?
Here's a more detailed example:
describe("Spy tracking", function() {
var calculator;
var logger = {
log: function(message) {
console.log(message);
}
};
beforeEach(function() {
calculator = new Calculator();
spyOn(logger, 'log');
});
it("should track method calls with arguments", function() {
logger.log("Adding 5");
logger.log("Adding 3");
expect(logger.log).toHaveBeenCalled();
expect(logger.log.callCount).toEqual(2);
expect(logger.log).toHaveBeenCalledWith("Adding 5");
expect(logger.log).toHaveBeenCalledWith("Adding 3");
});
});
Spies can also return specific values or chain through to the real implementation:
describe("Spy return values", function() {
var api = {
getData: function() {
return { status: "loading" };
}
};
it("should return a fake value", function() {
spyOn(api, 'getData').andReturn({ status: "success", data: [1, 2, 3] });
var result = api.getData();
expect(result.status).toEqual("success");
});
it("should call through to the real method", function() {
spyOn(api, 'getData').andCallThrough();
var result = api.getData();
expect(result.status).toEqual("loading");
expect(api.getData).toHaveBeenCalled();
});
it("should execute a custom function", function() {
spyOn(api, 'getData').andCallFake(function() {
return { status: "error", message: "Network error" };
});
var result = api.getData();
expect(result.status).toEqual("error");
});
});
Asynchronous Testing
JavaScript is full of asynchronous operations—AJAX calls, timers, animations. Jasmine handles these with runs, waits, and waitsFor blocks.
Let's test a function that fetches data with a timeout:
// src/DataFetcher.js
var DataFetcher = function() {
this.data = null;
};
DataFetcher.prototype.fetch = function(callback) {
var self = this;
setTimeout(function() {
self.data = { items: [1, 2, 3] };
callback(self.data);
}, 100);
};
Here's how to test it:
describe("DataFetcher async", function() {
var fetcher;
var result;
beforeEach(function() {
fetcher = new DataFetcher();
result = null;
});
it("should fetch data asynchronously", function() {
runs(function() {
fetcher.fetch(function(data) {
result = data;
});
});
waitsFor(function() {
return result !== null;
}, "Data should be fetched", 1000);
runs(function() {
expect(result).toBeDefined();
expect(result.items.length).toEqual(3);
});
});
});
The runs function executes a block of code. The waitsFor function polls a condition until it's true or times out. You pass it:
- A function that returns true when ready
- A failure message if it times out
- A timeout in milliseconds (optional, defaults to 5000)
You can also use waits to pause for a specific duration:
it("should wait for a specific time", function() {
var value = 0;
runs(function() {
setTimeout(function() {
value = 1;
}, 50);
});
waits(100);
runs(function() {
expect(value).toEqual(1);
});
});
Here's a more practical example testing an AJAX-like call:
var ApiClient = function() {};
ApiClient.prototype.get = function(url, callback) {
setTimeout(function() {
callback({ status: 200, data: "response" });
}, 100);
};
describe("ApiClient", function() {
var client;
var response;
beforeEach(function() {
client = new ApiClient();
});
it("should handle successful API calls", function() {
runs(function() {
client.get("/api/data", function(result) {
response = result;
});
});
waitsFor(function() {
return response !== undefined;
}, "API call should complete", 500);
runs(function() {
expect(response.status).toEqual(200);
expect(response.data).toEqual("response");
});
});
});
Custom Matchers
Sometimes the built-in matchers aren't enough. You can create custom matchers for domain-specific assertions:
beforeEach(function() {
this.addMatchers({
toBeValidEmail: function() {
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(this.actual);
},
toBeWithinRange: function(min, max) {
return this.actual >= min && this.actual <= max;
}
});
});
describe("Custom matchers", function() {
it("should validate email addresses", function() {
expect("[email protected]").toBeValidEmail();
expect("invalid.email").not.toBeValidEmail();
});
it("should check numeric ranges", function() {
expect(50).toBeWithinRange(0, 100);
expect(150).not.toBeWithinRange(0, 100);
});
});
Custom matchers make your tests more readable and expressive, especially when you're testing complex domain logic.
Disabling and Focusing Tests
During development, you might want to run only specific tests or temporarily disable others. Jasmine provides xdescribe and xit for this:
xdescribe("Disabled suite", function() {
it("will not run", function() {
expect(true).toBe(false);
});
});
describe("Active suite", function() {
it("will run", function() {
expect(true).toBe(true);
});
xit("will be skipped", function() {
expect(true).toBe(false);
});
});
Tests in xdescribe suites and xit specs show up as pending in the test results but don't execute. This is useful when you're working on a specific feature and don't want to wait for the entire test suite.
Organizing Large Test Suites
As your application grows, your test suite grows with it. Here are some tips for keeping things organized:
Group related tests in nested suites:
describe("User", function() {
describe("authentication", function() {
describe("with valid credentials", function() {
it("should log in successfully", function() {
// test
});
});
describe("with invalid credentials", function() {
it("should show error message", function() {
// test
});
});
});
describe("profile", function() {
// profile-related tests
});
});
Split specs into separate files by feature or module:
spec/
├── models/
│ ├── UserSpec.js
│ └── PostSpec.js
├── views/
│ ├── HeaderViewSpec.js
│ └── SidebarViewSpec.js
└── helpers/
└── UtilsSpec.js
Use shared examples for common behavior:
var itBehavesLikeAModel = function() {
it("should have an id", function() {
expect(this.model.id).toBeDefined();
});
it("should be saveable", function() {
expect(typeof this.model.save).toBe('function');
});
};
describe("User", function() {
beforeEach(function() {
this.model = new User();
});
itBehavesLikeAModel();
// User-specific tests
});
describe("Post", function() {
beforeEach(function() {
this.model = new Post();
});
itBehavesLikeAModel();
// Post-specific tests
});
Running Jasmine in Different Environments
While the standalone browser runner is great for getting started, you'll often want to integrate Jasmine into your build process.
Running in Node.js:
Install jasmine-node via npm and run your specs from the command line:
npm install jasmine-node -g
jasmine-node spec/
Continuous Integration:
Many CI servers can run Jasmine tests headlessly using PhantomJS or other headless browsers. This lets you run tests on every commit without manual intervention.
Integration with Rails:
If you're using Rails, the jasmine gem integrates Jasmine into your Rails app with a rake task and test server.
Best Practices
After working with Jasmine on several projects, here are some best practices I've learned:
Write descriptive test names. Your specs should read like documentation. Someone should be able to understand what your code does just by reading the test names.
Test one thing per spec. Each it block should verify one specific behavior. This makes failures easier to diagnose.
Don't test implementation details. Test the public interface and behavior, not internal state or private methods. This makes your tests more resilient to refactoring.
Use beforeEach for setup. Don't repeat setup code in every test. Put it in beforeEach to keep tests focused and DRY.
Keep tests fast. Slow tests discourage running them frequently. Use spies instead of real dependencies when possible, and avoid unnecessary setup.
Run tests frequently. Don't wait until you've written a lot of code to run tests. Run them after every change to catch problems early.
Test edge cases. Don't just test the happy path. Test with null values, empty arrays, invalid input, and boundary conditions.
Wrapping Up
Jasmine makes testing JavaScript pleasant and productive. Its BDD-style syntax creates tests that are readable and maintainable, while features like spies and asynchronous support handle real-world testing scenarios with ease.
The key to successful testing isn't just knowing the tools—it's making testing a natural part of your development workflow. Start by testing new code as you write it. When you fix a bug, write a test that would have caught it. Gradually add tests to existing code when you modify it.
I've found that good tests actually make me write better code. When something is hard to test, it's usually a sign that the design needs improvement. Tests push you toward smaller functions, looser coupling, and clearer interfaces.
Whether you're building a small jQuery plugin or a large Backbone.js application, Jasmine gives you the tools to write robust, well-tested JavaScript. Give it a try on your next project—you'll wonder how you ever worked without it.