Enough theory! Let's get our hands dirty with some practical examples using the Alcohology app from Chapter 9, A Shopping Application. First, we'll drill down into the guts of the application and build a unit test for some of the key business functionality. Then, we'll jump up to a high-level view and check whether our building blocks integrate correctly.
There are a wealth of tools we can use for both. The Ext JS framework has begun to build a set of unit tests to verify its behavior and cut out regressions in core functionality. To do so, Sencha has chosen the Jasmine library.
Jasmine is a behavior-driven framework, a term that relates to the way tests are described. Rather than using the "assertation" terminology, it uses the "expectation" format that we briefly mentioned earlier in the chapter. It asks us to specify behavior and expect a particular result. Here's the canonical example from Jasmine's documentation:
describe('A suite', function() { it('contains spec with an expectation', function() { expect(true).toBe(true); }); });
The describe
method encloses one or more specifications, themselves declared in an it
method, with expectations declared using the expect
method. To translate the previous code to plain language, use the following command:
We have "a suite", which "contains spec with an expectation". This expectation expects "true" to be "true".
Obviously, this is a contrived suite, as we'd hope that true would always be true! However, it should serve as a useful demonstration of the general syntax of a Jasmine test. Before we can get going and use this on our own application, we need to take a little bit of time to download and set up the Jasmine library.
The simplest way of getting started with Jasmine is to download the latest version from the project's release page. At the time of writing this book, the current version is 2.1.3. Refer to https://github.com/jasmine/jasmine/releases for more information.
Extract the ZIP file and you'll see that the download includes some example specifications that we don't need; let's clear these out from within the new Jasmine directory:
rm MIT.LICENSE spec/* src/*
Now, we can move the Jasmine library to the root of the Alcohology project, assuming our current directory is now in the Alcohology project:
mkdir ./testsmv ~/Downloads/jasmine-2.1.3 ./tests/jasmine
We can now fire up our application; if you've downloaded the project files, then the readme file will tell you to run npm start
and it'll start the Ext JS project and the API server. Once this is done, we can open http://localhost:1841/tests/jasmine/SpecRunner.html
in our browser to run the specs, as shown here:
In this screenshot, we can see the spec runner, but it's got nothing to do. We've got a little bit more configuration to do before we can start writing some specifications. Let's open up the SpecRunner.html
file in an editor and tweak it to look like this:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Jasmine Spec Runner v2.1.3</title> <link rel="stylesheet" href="lib/jasmine-2.1.3/jasmine.css"> <script src="lib/jasmine-2.1.3/jasmine.js"></script> <script src="lib/jasmine-2.1.3/jasmine-html.js"></script> <script src="lib/jasmine-2.1.3/boot.js"></script> <script src="../../ext/build/ext-all-debug.js"></script> <script type="text/javascript"> Ext.Loader.setConfig({ enabled: true, paths: { 'Alcohology': '../../app' } }); </script> <script src="spec/Cart.js"></script> </head> <body></body> </html>
This HTML file is really just a host for the Jasmine library, but it's also where we wire up Ext JS to work outside of the context of an application. By including the ext-all
JavaScript file and reconfiguring Ext.Loader
to grab any Alcohology classes from the correct directory, we can instantiate classes to test and Ext JS will automatically request the files we need from our application directory. All that's left to do is include the actual JavaScript specification files at the bottom of the head element. Here, we've already added a reference to spec/Cart.js
.
With all of the setup out of the way, we can move on to writing some tests!
Earlier, we wrote some pseudocode to illustrate how to test the addProduct
method on the cart store. Now, let's build out the real Jasmine specification that accomplishes this for real. We need to create a test suite with a cart store that'll be used as test subject:
describe('Cart store', function() { var cart; beforeEach(function() { cart = Ext.create('Alcohology.store.Cart'), }); });
Our first suite is simply called Cart store
. We have a cart variable that gets reassigned beforeEach
specification is run. It's assigned an instance of the cart store via Ext.create
. Thanks to our configuration in the previous section, Ext.create
will use Ext.Loader
to automatically pull in the relevant source code file, including any dependencies. By reinstantiating before every test, we can be sure that a test later in the suite won't be affected by the way an earlier test has manipulated the cart.
We can now sketch out the functionality we'd like to test. The following code goes after the beforeEach
call:
describe('#addProduct', function() { it('should accept a Product model'), it('should create a new CartItem line'), it('should increment the quantity when adding an existing Product'), it('should not create a new CartItem when adding an existing Product'), });
If we refresh the SpecRunner.html
page, then we'll actually be able to see something like this:
These specifications are just placeholders, but the fact that they show up in the runner is useful for developers practicing test first development. We can write a series of specification statements that describe the functionality we require, then the specs, and finally, the code itself. In this way, we're specifying the behavior we need and the code itself follows, and we can be safe in the knowledge that it meets our requirements. This can be a powerful methodology for an architect to spell out in detail how a class should behave.
Let's go through each spec one by one:
it('should accept a Product model', function() { expect(cart.addProduct.bind(cart, {})).toThrow(); });
We expect that if addProduct
is passed, something that is not a product model, it will throw an exception. We pass the method to the expect call prepopulated with an empty object literal. As this isn't a product model—as expected—it throws an exception and satisfies the test as follows:
it('should create a new CartItem line', function() { var product = Ext.create('Alcohology.model.Product'), cart.addProduct(product); expect(cart.count()).toBe(1); });
When the product is added to the cart, we expect that it will cause a new line item to be created in the store. We simply check whether the cart count is as expected after adding a product:
it('should increment the quantity when adding an existing Product', function() { var product = Ext.create('Alcohology.model.Product'), cart.addProduct(product); cart.addProduct(product); expect(cart.first().get('quantity')).toBe(2); });
After adding a product that's already in the cart, we expect that it will increase the quantity of the corresponding cart line. We pass in the same product twice and check whether the quantity is two, as expected:
it('should not create a new CartItem when adding an existing Product', function() { var product = Ext.create('Alcohology.model.Product'), cart.addProduct(product); cart.addProduct(product); expect(cart.count()).toBe(1); });
This is a similar setup as the last test, but we are expecting that there will not be a duplicated cart line, but instead, there will be only one item in the cart.
With all of the specifications written, we can refresh the spec runner again:
As you can see, the specs are all in green, indicating that they have passed successfully.
This is just a brief primer on unit testing with Jasmine, but it demonstrates the power available and the utility of testing in this manner. It gives us confidence in the code we've written and ensures that any additions to the addProduct
method won't break the functionality that already exists.