If you pride yourself on possessing the 3 Virtues of a Great Programmer, then you probably hate writing automated test suites. Writing comprehensive suites of Unit tests flies directly in the face of everything we Great Programmers stand for. Why would a lazy, impatient developer who never makes mistakes need to waste all that time, anyway?
Most of the time, all you want is an automated way to make sure your application is functioning properly at a high level. Do you really need to write mountains of Unit tests to do this? Can Unit tests even achieve this at all? How do you even write effective tests for front-end behavior?
Fortunately for you, virtuous developer, it’s 2013 and there are some awesome tools to help you achieve your testing goals without wanting to pull your hair out or strangle some poor, pontificating TDD extremist.
CasperJS is the answer to the prayers you would have prayed if you weren’t too lazy and impatient to bother. But first, a word about Unit Testing versus Functional Testing.
Unit Tests vs. Functional Tests
Let’s review. There are basically 2 kinds of automated tests: Unit and Functional. Unit tests are written from the developer’s perspective, and typically target a method or a class (did my
sort method sort properly?). Functional tests (sometimes referred to as Integration tests by those who prefer the extra syllable) are written from the user’s perspective, and usually test the interaction between multiple building blocks of the application (is the user able to log in? Can she update her account?).
In a perfect world, your application would have both a comprehensive suite of Unit tests as well as a bunch of Functional tests exercising your application’s features. In reality, you’ve probably got very little to none of either. If you had a full set of Unit and Functional tests, you probably wouldn’t be reading an article title “Front-end Testing for the Lazy Developer” now, would you?
Functional Tests > Unit Tests
Let’s face it. Writing Unit tests just isn’t very fun, and despite what the idealist proponents of TDD may say, it can sometimes take just as long to write the test as it did to write the actual code. And will your users be impressed that you have 90% code coverage and that all of your unit tests are passing when you have pages in your app that show up blank or errors that render your application unusable? No one cares that your Unit tests are passing if your application doesn’t function.
Unit testing makes sure you are using quality ingredients. Functional testing makes sure your application doesn’t taste like crap.
Programmers solve big problems by breaking them down into smaller problems. And as anyone who has ever worked on a non-trivial application knows, the complexity lies in the interaction between the pieces of your application, not in the pieces themselves (if that’s not true, you need smaller pieces).
You may catch some bugs and catch them early with your Unit tests. Great! But bugs feed off of complexity, which means you’ll find them where things are the most complicated - in the interplay of your application’s various parts. Unit tests can’t help you there.
Especially in the Browser
So in addition to the downsides and limitations of Unit tests generally, on the front-end you have extra overhead involved in writing time-consuming Unit tests that will never ensure your application actually works.
Functional testing lets you test large swaths of code with considerably less effort and looser coupling. A Functional test tests that entering username and password into a form and clicking submit results in a successful login. A Unit test calls a specific method on a specific object and examines the result. When that method name changes or the object goes away, your test is broken. Functional tests are far less likely to break when your underlying implementation changes.
Unit testing has its place. I don’t want to discourage you from ever writing unit tests. It just seems like Unit testing gets all of the attention, when Functional testing provides a far bigger bang for your buck. Again, ideally you would do both! But in the real world, most developers barely have time for one kind of automated test, let alone two. If that describes you, I would strongly encourage you to go with Functional testing.
CasperJS and PhantomJS
In web applications, you can’t automatically test your front-end without using some sort of tool. There are a number of options out there, like Selenium, which has been around forever and has a nice GUI. If you want something lightweight and simple that you can run from the command line, CasperJS may be the right tool for you.
Casper runs on PhantomJS, a headless WebKit browser, and provides a full-featured API that lets you interact with a Phantom instance pointing at your application. So rather than setting up special test pages that run your Unit tests, you can test your actual application. For a
lazy virtuous developer like me, this is great.
evaluate method (more on this later).
casperjs my-source.js. If you will be running unit tests, you must include the
test command, like so:
casperjs test my-test.js. All of the examples in this post should be run with the
test command. If you want to follow along or run these tests locally, you can download the code from this GitHub repo.
Casper has a fantastic API full of convenience methods to help you interact with your phantasmic browser. There are two main modules that you can use, the casper module and the tester module. Methods in the tester module are only available when you run Casper with
casperjs test my-test.js.
Let’s first look at what the main
casper module can do, then we’ll look at tests in particular. To get started, let’s open https://www.reddit.com/, print the page’s title, and take a screenshot.
Simple enough. the
start() call opens the page and executes the callback when it’s loaded. We then take a screenshot and save it to a PNG called reddit-home.png. Here’s what it looks like:
Saving screenshots in this manner can be a great part of your functional tests, and can be hugely helpful as a debugging tool when writing your tests, helping you “see” what’s going on in the invisible browser.
Casper Test API
Now let’s take a look at Casper’s test API. Let’s open up the /r/programming subreddit, click the “New” link and confirm that we’re on the right page and have the correct content.
Here, after we click the “New” link, we wait for the url to change and a new page to load. Then we confirm that there are 25 links on the page, the Reddit default.
Casper's API is chock full of handy helper methods like
assertElementCount(). Let's look at a more complicated method,
Invading the DOM -
Sometimes, to really test something complicated, you need to jump into the DOM of the browser itself. Casper provides the ability to do just that with the
[evaluate()](http://docs.casperjs.org/en/latest/modules/casper.html#evaluate) method. If you find this a bit confusing, they have a nice diagram to help you picture this.
Here’s an example where we will jump into the context of the r/programming page, click on upvote, confirm that the login modal appears, then click on the
.close backdrop and confirm that the modal disappears. The results are then returned to the casper environment.
An important thing to note when using
casper.evaluate() is that unlike almost any other interaction that occurs with the browser (
evaluate is synchronous. Whereas after calling
fill or ’sendKeys you will need to wrap your next interaction in a
evaluate happens instantly. This actually makes it easier to use than some of its asynchronous counterparts, but after a while you get used to asynchronous and have to remind yourself that evaluate is synchronous.
Thoughts on using Casper
Probably the most difficult part of using Casper is dealing with the asynchronous nature of pretty much everything. It is important to understand that
then() essentially adds navigation steps to a stack. So if you call
click(), you will need to wrap your follow-up code in a
then() callback in order to see the changes from your click event. I recommend reading helpful links like this StackOverflow question to try to wrap your head around this concept.
Something I have found helpful in writing medium to large test suites is to make certain to organize code into separate files with specific purposes. This is easy, since the PhantomJS execution environment allows you to import/require other JS files using the CommonJS syntax. So if you have a utility file my-utils.js, you can import it and use it elsewhere.
This is incredibly helpful, and allows you to organize your app into smaller pieces like you would in a “real” application. I have found it helpful to organize in these ways:
- test.js - put your initial configuration here, and require() and run your tests here. Keep it as light as possible.
- my-utils.js - have a common utility class for common actions and helper methods
- navigation.js - separate your navigation (go to this page) from your tests, and make navigation reusable across tests. For instance,
require('./navigation').homePage()takes you to, you guessed it, the home page.
- customer.js - test the “customer” section of your application. Keep this separate from login test and all of the other sections of your application you will be besting.
Basically, when you are writing your test suites, don’t disregard all of your hard-won knowledge about good software engineering best practices because you are “just” writing tests. YAGNI. DRY. You know the drill.
If you’ve never really written unit or functional tests before, I hope you’ll bite the bullet, take an hour and give Casper a shot. It has a great community, is actively maintained, and makes for a great, lightweight and code-centric solution to a difficult and important problem that gets addressed far too infrequently. If you’re disillusioned with Unit Tests and TDD, don’t throw the Functional Testing baby out with the proverbial bathwater. I think you’ll find the ROI for Casper integration testing to be far higher than you ever expected.
If you’d like more information on Casper, check out these links:
Join 251,101 readers who are obsessed with delivering great customer service.