So we’ve previously seen how to bootstrap a chrome extension with CoffeeScript. The next step is about adding testing support. Even if it’s a simple extension, the whole process of reloading the extension in the browser to manually test a feature is incredibly boring and error prone.
{% end_excerpt %}
To illustrate,
TabSwitcher will be used as an example, more
specifically the match(url, abbreviation)
function which returns a float
, indicating how close is
abbreviation
is to url
.
This post assume you’re already comfortable with testing.
Setuping the tests
Before anything, a test framework needs to be chosen. Mocha is probably the most popular choice nowadays. It doesn’t come with an expectation library and defaults to NodeJS’s standard library : assert.
As you may know javascript Arrays being objects, testing equality between objects of the same type
means testing identity. In short [1,2] == [1,2]
evaluates to false
.
It does make sense given how the equality operator is defined yet being
unusual to someone used to Ruby or Python.
Most of the time, it’s equality between elements that is needed when writing assertions.
A bad practice to achieve it would be to use toString()
, converting them and
running the comparison against the returned strings. It’s poor man’s
equality because it would skip elements types, ie comparing [1,2]
and ['1','2']
returns true
instead of the expected false
.
Node’s assert library is obviously relying on the equality operator, it would yield false positives which are really painful to deal with.
To avoid wasting time plumbing with arrays comparisons, let’s just use an assertion library, Mocha advertises about should.js which provides our expected array comparison:
{% highlight coffeescript %} it “should success”, -> [1,2].should.eql([1,2]) {% endhighlight %}
To install these two libraries:
{% highlight sh %} npm install mocha should -g {% endhighlight %}
Storing tests under /tests
directory is a no brainer. You can place
that directory wherever you want, /spec
being a common choice
too.
{% highlight sh %} cd tabswitcher # extension’s root dir mkdir tests {% endhighlight %}
And create a file that will contain our tests. {% highlight sh %} touch spec/fuzzy_test.coffee {% endhighlight %}
{% highlight coffeescript %} require ‘should’
describe “Fuzzy”, -> it “should fail”, -> 0.should.eql(42) {% endhighlight %}
Run it with {% highlight sh %} mocha –compilers coffee:coffee-script tests
.
X 1 of 1 test failed:
Fuzzy should fail: actual expected
42 0 at Object.Assertion.eql
(/home/tech/code/jhchabran/tabswitcher/node_modules/should/lib/should.js:285:10) at Context. (/home/tech/code/jhchabran/tabswitcher/tests/fuzzy_test.coffee:8:24) at Test.Runnable.run (/usr/local/lib/node_modules/mocha/lib/runnable.js:213:32) at Runner.runTest (/usr/local/lib/node_modules/mocha/lib/runner.js:341:10) at Runner.runTests.next (/usr/local/lib/node_modules/mocha/lib/runner.js:387:12) at next (/usr/local/lib/node_modules/mocha/lib/runner.js:267:14) at Runner.hooks (/usr/local/lib/node_modules/mocha/lib/runner.js:276:7) at next (/usr/local/lib/node_modules/mocha/lib/runner.js:224:23) at Runner.hook (/usr/local/lib/node_modules/mocha/lib/runner.js:244:5) at process.startup.processNextTick.process._tickCallback (node.js:244:9) {% endhighlight %}
A test task in the Cakefile comes handy to fire the tests.
Loading the code we want to test
Now tests are up and failing, it’s time to write expectations about functions that will be used in the extension. There is a last bump on our road to deal with. Code is ran in two different environments : browser and NodeJs.
On one hand, the chrome extension code runs in a browser, scripts are loaded by the html
pages, through script
tags. On the other hand tests
run the code in a Node.js environment and loading is done through
require
and exports
object.
Given src/fuzzy.coffee
is going to define a match
function
we need to use in our tests, it first must be exported to be succesfully required
afterward.
{% highlight coffeescript %}
/src/fuzzy.coffee
match = (url, abbreviation)->
…
exports.match = match
{% endhighlight %}
{% highlight coffeescript %}
/tests/fuzzy_spec.coffee
f = require ‘../src/fuzzy’
describe “Fuzzy”, -> it “should call match”. -> score = f.match “http://google.com”, “ggl” score.should.eql(3) {% endhighlight %}
We run it again with cake test
and it should fail or success
depending the implementation of match
.
But if the extension is launched within the browser it will whine about
exports
not being defined. No surprise, in a browser
context,exports
doesn’t mean anything unless manually defined.
{% highlight coffeescript %}
Idiomatic predicate
isCommonJS = typeof(window) == “undefined”
if isCommonJS exports.match = match else window.match = match {% endhighlight %}
And now we got our code both running in specs and in the extension.
What about Chrome API ?
Extension code that makes use of chrome.* apis that can’t be used in tests as is because they belong to Chrome and we’re running tests unde NodeJs.
Usually simply separating concerns solves the problem. In the
present case, match
just returns a float, indicating how close the
abbreviation is to the url. No api calls are made through computation,
leaving no need to mock them in some sort.
Sometimes, for simplicity sake, it’s shorter to still pass around chrome
api structures, like chrome.tab
. Well, it’s just an object, build
one that mimics the fields you need and that’s all.
{% highlight coffeescript %}
/tests/fuzzy_spec.coffee
…
tab = url : “http://google.com” title : “Google”
…
{% endhighlight %}
It’s coding time!
We got our extension running CoffeeScript code and tests can be written as needed. Isn’t it more comfortable to write a Chrome Extension now ?
As we just added unit testing so far, an interesting question would be how to write some integration tests ? Like testing the whole workflow, considering we must deal with code running in different contexts: background page, popup and inserted in the current page.
You can find the example used through this post on Github.