We all know we should be testing our code, but we don’t actually do it. I guess it’s fair to say that most of us put it off because, nine times out of ten, it means learning yet another concept. In this tutorial, I’ll introduce you to a great little framework for testing your JavaScript code with ease.
By the way, did you know that you can have your JavaScript errors fixed quickly and easily by an expert on Envato Studio?
ThemeManiac, for example, will fix JavaScript errors or browser compatibility issues on your website or web application. The fixes can be completed very fast, based on the complexity and available information. He can also reorganize your scripts and make a completely new user experience. He has completed more than 1,000 jobs on Envato Studio, with 99% of customers recommending him.
Step 0: Understanding BDD
Today, we’re going to be learning about the Jasmine BDD testing framework. But we’re stopping here for a detour first, to talk very briefly, about BDD and TDD. If you’re not familiar with these acronyms, they stand for Behaviour-Driven Development and Test-Driven Development. I’m in the middle of learning about what each of these is in practice and how they are different, but here are some of the basic differences:
BDD and TDD … stand for Behaviour-Driven Development and Test-Driven Development.
TDD in its simplest form is just this:
- Write your tests
- Watch them fail
- Make them pass
- Refactor
- Repeat
That’s pretty easy to understand, eh?
BDD is a little more complex: as I understand it right now, I don’t think that you or I as a single developer can actually practice it fully; it’s more of a team thing. Here are a few of the practices of BDD:
- Establishing the goals of different stakeholders required for a vision to be implemented
- Involving stakeholders in the implementation process through outside-in software development
- Using examples to describe the behavior of the application, or of units of code
- Automating those examples to provide quick feedback and regression testing
To learn more, you can read the extensive Wikipedia Article (from which those points were taken).
All this to say that, while Jasmine bills itself as a BDD framework, we’re going to be using it in a more TDD-style way. That doesn’t mean we’re using it wrong, though. Once we’re finished, you’ll be able to test your JavaScript with ease … and I expect you to do it!
Step 1: Learning the Syntax
Jasmine takes a lot of cues from Rspec.
If you’re at all familiar with Rspec, the de facto BDD framework, you’ll see that Jasmine takes a lot of cues from Rspec. Jasmine tests are primarily two parts: describe
blocks and it
blocks. Let’s see how this works.
We’ll look at some closer-to-real-life tests in a few, but for now, we’ll keep it simple:
describe('JavaScript addition operator', function () { it('adds two numbers together', function () { expect(1 + 2).toEqual(3); }); });
Both the describe
and it
functions take two parameters: a text string and a function. Most test frameworks try to read as much like English as possible, and you can see this with Jasmine. First, notice that the string passed to describe
and the string passed to it
form a sentence (of sorts): “JavaScript addition operator adds two numbers together.” Then, we go on to show how.
Inside that it
block, you can write all the setup code you need for your test. We don’t need any for this simple example. Once you’re ready to write the actual test code, you’ll start with the expect
function, passing it whatever you are testing. Notice how this forms a sentence as well: we “expect 1 + 2 to equal 3.”
But I’m getting ahead of ourselves. As I said, whatever value you pass into expect
will be tested. The method you call, off the returned value of expect
, will be determined by which test is run. This group of methods is called ‘matchers’, and we’ll be looking at several of them today. In this case, we’re using the toEqual
matcher, which checks to see that the value passed to expect
and the value passed to toEqual
are the same value.
I think you’re ready to take this to the next level, so let’s set up a simple project using Jasmine.
Step 2: Setting up a Project
Jasmine can be used by itself; or you can integrate it with a Rails project. We’ll do the former. While Jasmine can run outside the browser (think Node, among other places), we can get a really nice little template with the download.
So, head on over to the standalone download page and get the latest version. You should get something like this:
You’ll find the actual Jasmine framework files in the lib
folder. If you prefer to structure your projects differently, please do so; but we’re going to keep this for now.
There’s actually some sample code wired up in this project template. The “actual” JavaScript ( the code we want to test) can be found in the src
subdirectory; we’ll be putting ours there shortly. The testing code—the specs—go in the spec
folder. Don’t worry about the SpecHelper.js
file just yet; we’ll come back to that.
That SpecRunner.html
file is what runs the tests in a browser. Open it up (and check the “passed” checkbox in the upper right corner), and you should see something like this:
This shows us that all the tests for the sample project are passing. Once you get through this tutorial, I recommend you open up the spec/PlayerSpec.js
file and peruse that code. But right now, let’s give this test writing stuff a try.
- Create
convert.js
in thesrc
folder. - Create
convertSpec.js
in thespec
folder, - Copy the
SpecRunner.html
file and rename itSpecRunner.original.html
. -
Remove the links to the sample project files in
SpecRunner.html
and add these lines:<script src="src/convert.js"><>/script> <script src="spec/convertSpec.js"></script>
Now we’re ready to create a mini-library that will convert between measurement units. We’ll start by writing the tests for our mini-library.
Step 3: Writing the Tests
So, let’s write our tests, shall we?
describe( "Convert library", function () { describe( "distance converter", function () { }); describe( "volume converter", function () { }); });
We start with this; we’re testing our Convert
library. You’ll notice that we’re nesting describe
statements here. This is perfectly legal. It’s actually a great way to test seperate functionality chunks of the same codebase. Instead of two seperate describe
calls for Convert
library’s distance conversions and volume conversions, we can have a more descriptive suite of tests like this.
Now, onto the actual tests. I’ll repeat the inner describe
calls here for your convenience.
describe( "distance converter", function () { it("converts inches to centimeters", function () { expect(Convert(12, "in").to("cm")).toEqual(30.48); }); it("converts centimeters to yards", function () { expect(Convert(2000, "cm").to("yards")).toEqual(21.87); }); });
Here are our tests for distance conversions. It’s imporant to notice something here: we haven’t written a speck of code for our Convert
library yet, so in these tests we’re doing more than just check to see if it works: we’re actually deciding how it will be used (and therefore implemented). Here’s how we’ve decided to make our conversions:
Convert(<number>, <from unit string>).to(<to unit string>);
Yes, I’m taking a cue from the way Jasmine has implemented its tests, but I think it’s a nice format. So, in these two tests, I’ve made the conversions myself (ok, with a calculator) to see what the results of our calls should be. We’re using the toEqual
matcher to see if our tests pass.
Here’s the volume tests:
describe( "volume converter", function () { it("converts litres to gallons", function () { expect(Convert(3, "litres").to("gallons")).toEqual(0.79); }); it("converts gallons to cups", function () { expect(Convert(2, "gallons").to("cups")).toEqual(32); }); });
And I’m going to add two more tests in our top-level describe
call:
it("throws an error when passed an unknown from-unit", function () { var testFn = function () { Convert(1, "dollar").to("yens"); } expect(testFn).toThrow(new Error("unrecognized from-unit")); }); it("throws an error when passed an unknown to-unit", function () { var testFn = function () { Convert(1, "cm").to("furlongs"); } expect(testFn).toThrow(new Error("unrecognized to-unit")); });
These check for errors that should be thrown when unknown units are passed into either the Convert
function or the to
method. You’ll notice that I’m wrapping the actual conversion in a function and passing that to the expect
function. That’s because we can’t call the function as the expect
parameter; we need to hand it a function and let it call the function itself. Since we need to pass a parameter to that to
function, we can do it this way.
The other thing to note is that I’m introducing a new matcher: toThrow
, which takes an error object. We’ll look at some more matchers soon.
Now, if you open SpecRunner.html
in a browser, you’ll get this:
Great! Our tests are failing. Now, let’s open our convert.js
file and do some work:
function Convert(number, fromUnit) { var conversions = { distance : { meters : 1, cm : 0.01, feet : 0.3048, inches : 0.0254, yards : 0.9144 }, volume : { liters : 1, gallons: 3.785411784, cups : 0.236588236 } }, betweenUnit = false, type, unit; for (type in conversions) { if (conversions(type)) { if ( (unit = conversions[type][fromUnit]) ) { betweenUnit = number * unit * 1000; } } } return { to : function (toUnit) { if (betweenUnit) { for (type in conversions) { if (conversions.hasOwnProperty(type)) { if ( (unit = conversions[type][toUnit]) ) { return fix(betweenUnit / (unit * 1000)); } } } throw new Error("unrecognized to-unit"); } else { throw new Error("unrecognized from-unit"); } function fix (num) { return parseFloat( num.toFixed(2) ); } } }; }
We’re not really going to discuss this, because we’re learning Jasmine here. But here are the main points:
- We’re making the conversions by storing the conversion in an object; conversion numbers are classified by type (distance, volume, add your own). For each field of measurement, we have a base value (meters or liters, here) that everything converts to. So when you see
yards: 0.9144
, you know that that’s how many yards there are in a meter. Then, to convert yards to, say, centimeters, we multiplyyards
by the first parameter (to get the number of meters) and then divide the product bycm
, the number of meters in a centimeter. This way, we don’t have to store the conversion rates for every pair of values. This also makes it easy to add new values later. - In our case, we’re expecting the units passed in to be the same as the keys we’re using in the conversion “table.” If this were a real library, we’d want to support multiple formats—like ‘in’, ‘inch’, and ‘inches’—and therefore we’d want to add some logic to match the
fromUnit
to the right key. - At the end of the
Convert
function, we store the intermediate value inbetweenUnit
, which is initialized tofalse
. That way, if we don’t have thefromUnit
,betweenUnit
will befalse
going into theto
method, and so an error with be thrown. - If we don’t have the
toUnit
, a different error will be thrown. Otherwise, we’ll divide as neccessary and return the converted value.
Now, go back to SpecRunner.html
and reload the page. You should now see this (after checking “Show passed”):
There you go! Our tests are passing. If we were developing a real project here, we would write tests for a certain chunk of functionality, make them pass, write tests for another check, make them passs, etc. But since this was a simple example, we’ve just done it all in one fell swoop.
And now that you’ve seen this simple example of using Jasmine, let’s look at a few more features that it offers you.
Step 4: Learning the Matchers
So far, we’ve used two matchers: toEqual
and toThrow
. There are, of course, many others. Here are a few you’ll probably find useful; you can see the whole list on the wiki.
toBeDefined / toBeUndefined
If you just want to make sure a variable or property is defined, there’s a matcher for that. There’s also one to confirm that a variable or property is undefined
.
it("is defined", function () { var name = "Andrew"; expect(name).toBeDefined(); }) it("is not defined", function () { var name; expect(name).toBeUndefined(); });
toBeTruthy / toBeFalsy
If something should be true or false, these matchers will do it.
it("is true", function () { expect(Lib.isAWeekDay()).toBeTruthy(); }); it("is false", function () { expect(Lib.finishedQuiz).toBeFalsy(); });
toBeLessThan / toBeGreaterThan
For all you number people. You know how these work:
it("is less than 10", function () { expect(5).toBeLessThan(10); }); it("is greater than 10", function () { expect(20).toBeGreaterThan(10); });
toMatch
Have some output text that should match a regular expression? The toMatch
matcher is ready and willing.
it("outputs the right text", function () { expect(cart.total()).toMatch(/\$\d*.\d\d/); });
toContain
This one is pretty useful. It checks to see if an array or string contains an item or substring.
it("should contain oranges", function () { expect(["apples", "oranges", "pears"]).toContain("oranges"); });
There are a few other matchers, too, that you can find in the wiki. But what if you want a matcher that doesn’t exist? Really, you should be able to do just about anything with some set-up code and the matchers Jasmine provides, but sometimes it’s nicer to abstract some of that logic to have a more readable test. Serendipitously (well, actually not), Jasmine allows us to create our own matchers. But to do this, we’ll need to learn a little something else first.
Step 5: Covering Before and After
Often—when testing a code base—you’ll want to perform a few lines of set-up code for every test in a series. It would be painful and verbose to have to copy that for every it
call, so Jasmine has a handy little feature that allows us to designate code to run before or after each test. Let’s see how this works:
describe("MyObject", function () { var obj = new MyObject(); beforeEach(function () { obj.setState("clean"); }); it("changes state", function () { obj.setState("dirty"); expect(obj.getState()).toEqual("dirty"); }) it("adds states", function () { obj.addState("packaged"); expect(obj.getState()).toEqual(["clean", "packaged"]); }) });
In this contrived example, you can see how, before each test is run, the state of obj
is set to “clean”. If we didn’t do this, the changed made to an object in a previous test persist to the next test by default. Of course, we could also do something similar with the AfterEach
function:
describe("MyObject", function () { var obj = new MyObject("clean"); // sets initial state afterEach(function () { obj.setState("clean"); }); it("changes state", function () { obj.setState("dirty"); expect(obj.getState()).toEqual("dirty"); }) it("adds states", function () { obj.addState("packaged"); expect(obj.getState()).toEqual(["clean", "packaged"]); }) });
Here, we’re setting up the object to begin with, and then having it corrected after every test. If you want the MyObject
function so you can give this code a try, you can get it here in a GitHub gist.
Step 6: Writing Custom Matchers
Like we said earlier, customer matchers would probably be helpful at times. So let’s write one. We can add a matcher in either a BeforeEach
call or an it
call (well, I guess you could do it in an AfterEach
call, but that wouldn’t make much sense). Here’s how you start:
beforeEach(function () { this.addMatchers({ }); });
Pretty simple, eh? We call this.addMatchers
, passing it an object parameter. Every key in this object will become a matcher’s name, and the associated function (the value) will be how it is run. Let’s say we want to create a matcher that with check to see if one number is between two others. Here’s what you’d write:
beforeEach(function () { this.addMatchers({ toBeBetween: function (rangeFloor, rangeCeiling) { if (rangeFloor > rangeCeiling) { var temp = rangeFloor; rangeFloor = rangeCeiling; rangeCeiling = temp; } return this.actual > rangeFloor && this.actual < rangeCeiling; } }); });
We simply take two parameters, make sure the first one is smaller than the second, and return a boolean statement that evaluates to true if our conditions are met. The important thing to notice here is how we get a hold of the value that was passed to the expect
function: this.actual
.
it("is between 5 and 30", function () { expect(10).toBeBetween(5, 30); }); it("is between 30 and 500", function () { expect(100).toBeBetween(500, 30); });
This is what the SpecHelper.js
file does; it has a beforeEach
call that adds the matcher tobePlaying()
. Check it out!
Conclusion: Having Fun Yourself!
There’s a lot more you can do with Jasmine: function-related matchers, spies, asynchronous specs, and more. I recommend you explore the wiki if you’re interested. There are also a few accompanying libraries that make testing in the DOM easier: Jasmine-jQuery, and Jasmine-fixture (which depends on Jasmine-jQuery).
So if you aren’t testing your JavaScript so far, now is an excellent time to start. As we’ve seen, Jasmine's fast and simple syntax makes testing pretty simple. There’s just no reason for you not to do it, now, is there?
If you want to take your JavaScript development further, why not check out the range of JavaScript items on Envato Market? There are thousands of scripts, apps and code snippets to help you.
Comments