Outside in Testing
Here at Cloudspace we do a lot of testing and as of late, we’re becoming big fans of the outside in testing approach. The general idea is that you write the user story, write a test, and let the code emerge from this structure. This works well since most of our work is highly immersed in client communication and feedback. For our Ruby and mobile developers, this has been a solved problem. We just use Cucumber.
Jasmine, so much Jasmine.
For unit testing Angular directives, it’s…
We need to inject a whole bunch of things into our test, get the template, load the template, compile it, digest it, and then run our tests. This is too much for a unit test.
Yes, you could probably leverage a tool like Selenium or Phantom for this and then test against a live version of the directive. That would cut out a large chunk of code, but now you need to call out to different parts of your project, which is just as bad for a unit test. You could create a test project that’s full of directives to test against, but now you’ve built a new project that needs to be maintained along side the actual project.
At this point you need write acceptance tests and there are much better tools and workflows for handling those, like Cucumber.
So, we found that the wonderful people who built Cucumber, built a node backed implementation of it.
You have your features.
You have a world (config) file, possibly some hooks.
And you have your steps.
And there’s more
This also provides acceptance tests for markup not in a custom directive! We can test how we utilize the baked in Angular directives in our views. It’s a win win. Also, considering that the rest of our team already use traditional Cucumber, we can all be on the same page.
- Make sure you have node and npm installed.
npm install -g cucumberfor global
npm install cucumber --save-devto install locally as a dependency
cdto the root of your project
- Add your features, step definitions, and config
- Run tests with
From here we need to talk about how to structure the testing directories.
You should shoot for directory structure like this.
Cucumber.js looks for step definitions in all sibling and child directories of the feature files themselves. Anything in a cousin directory will not be detected. So the following, would fail.
There are ways around this. By using a required flag, you can include step definitions from any location.
cucumber.js --require my/path
This could be useful as well for using common libraries of shared testing steps that may exist in another repository.
Node Exports and Require
Cucumber.js makes use of nodes export module system. All Cucumber components that you write are functions that are then exported, with the exception of your feature files.
Makes the function, object, or value available for requirement
module.exports = MyComponent;
Includes the function, object, or value in your code
MyComponent = require('./mycomponent.js');
Cucumber.js makes use of a World property that every step must use. This property is set to a function that contains all of the testing configuration for the suite.
Here is an example world
As you can see, we’re requiring all of our custom hooks, and any npm packages we want to leverage in our tests. Here we make use of “zombie” for browser interaction, as well as “should” for writing cleaner tests.
You could very much substitute any other web driver for zombie though. It’s only being used here for example purposes.
In Cucumber.js you need to manually relinquish control of your step definition when you are finished. This is true of everything in the testing suite from your world constructor, your steps, to your hooks.
callback()Release control of the step and move to the next action
callback.pending()Declare that the step hasn’t been written yet
callback.fail(new Error('Error'));Force an error, relinquish control, and pass an error message
The call to callback should probably be the last line of your function.