FuncUnit and Cucumber - a perfect combo for frontend testing
This weekend I spent some time playing with FuncUnit and Cucumber. In this article I'll explain why I like this combination, and how it enables you to super charge your frontend tests.
Cucumber
From the Cucumber wiki:
Cucumber is a tool that executes plain-text functional descriptions as automated tests.
Cucumber allows you to define your functionality in plain text and then run tests based on those definitions. Its main feature is that it allows BDD style of development, while allowing non technical stakeholders to write feature definitions.
I started by defining the list of features my component should implement. Since I decided to build a multiselect component, all of my features are related to selecting and deselecting items:
Feature: Deselecting items
Scenario: Deselect all items
Given A multiselect widget with selected items
When User clicks on the deselect all items button
Then All items are removed from the selected items list
And All items are returned to the not selected items list
...
After defining features, next step is writing tests. FuncUnit is an especially good fit for this case since it allows you simulate user behavior with an easy and clean syntax.
FuncUnit
From the FuncUnit homepage:
FuncUnit enhances assertion libraries like QUnit and Jasmine, enabling them to simulate user actions, easily test asynchronous behavior, and support black box testing.
FuncUnit has two main concepts:
- Actions - used to simulate user behavior like clicking, typing, dragging or scrolling.
- Waits - functions that will wait until a condition is true, or they will timeout and fail the test.
Here's an example:
F('#login').click();
F('#login-modal').exists("Login modal appeared");
FuncUnit uses jQuery - like syntax, so here's what we did in the example above:
- We clicked on the element with the
login
id - We waited until the element with the id
#login-modal
appeared on the page, and then asserted true with the "Login modal appeared" message
FuncUnit + Cucumber
I created a bit of glue code that allows you to use Cucumber and FuncUnit together (with QUnit as the assertion lib), and it allowed me to write my tests like this:
...
this.Given('A multiselect widget with selected items', function(next){
F('.selected-items-wrap li').size(1, function(){
ok(true, self.getCurrentStepName());
next();
});
})
this.When('User clicks on the selected item checkbox', function(next){
selectedItem = can.trim($('.selected-items-wrap li:first').text());
F('.selected-items-wrap input[type=checkbox]:first').click(function(){
ok(true, self.getCurrentStepName());
next();
})
})
...
Each step is wrapped in a Given/When/Then block, and it does as little as possible to satisfy the current step definition.
When passing the callback function argument to the FuncUnit action or wait, FuncUnit will ensure that this function is called only when the action or wait was successful.
We use the callback function to report the status of the test and call the next
function which will call the next step.
All these assertions are ran inside the QUnit's test, which is setup to expect an equal number of assertions as there are steps in the scenario.
The test runner glue code is pretty short (~100 lines), but I believe it allows a pretty nice API.
Results of the tests
Super charging the tests
After getting all of this to work, I got another idea: What if we could reuse these tests as the form of documentation?
Feature definitions are already written in a language that can be understood by non-technical people, so the only thing left is to somehow demonstrate what it would look like if someone used the compoment.
That turned out to be pretty easy to accomplish. I've extended the FuncUnit action functions so every time it simulates the click or typing (or any other user action) it will first move the div with the fake cursor to that element. In my opinion the result is pretty awesome:
Click on the scenario in the sidebar to run it as a demo
You could use this approach to demo the functionality to your customer and to have a living documentation of your components. You can find the code powering this demo on the GitHub.
If you have any comments or questions, either leave them below or you can contact me via my email.