We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Atom?
Atom uses Jasmine as its spec framework. Any new functionality should have specs to guard against regressions.
Atom specs and package specs are added to their respective spec
directory. The example below creates a spec for Atom core.
Spec files must end with -spec
so add sample-spec.coffee
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function() {
// contents
})
or
describe("Editor::moveUp", function() {
// contents
})
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function() {
it("has some expectations that should pass", function() {
// Expectations
})
})
The best way to learn about expectations is to read the Jasmine documentation about them. Below is a simple example.
describe("when a test is written", function() {
it("has some expectations that should pass", function() {
expect("apples").toEqual("apples")
expect("oranges").not.toEqual("apples")
})
})
In addition to the Jasmine's built-in matchers, Atom includes the following:
toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domThese are defined in spec/spec-helper.coffee.
Writing Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Atom. You can use our waitsForPromise
function.
describe("when we open a file", function() {
it("should be opened in an editor", function() {
waitsForPromise(function() {
atom.workspace.open('c.coffee').then(editor => expect(editor.getPath()).toContain('c.coffee'))
})
})
})
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function() {
beforeEach(function() {
waitsForPromise(() => atom.workspace.open('c.coffee'))
})
it("should be opened in an editor", function() {
expect(atom.workspace.getActiveTextEditor().getPath()).toContain('c.coffee')
})
})
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function() {
beforeEach(function() {
waitsForPromise(() => atom.workspace.open('sample.js'))
waitsForPromise(() => atom.packages.activatePackage('tabs'))
waitsForPromise(() => atom.packages.activatePackage('tree-view'))
});
it('should have waited long enough', function() {
expect(atom.packages.isPackageActive('tabs')).toBe(true)
expect(atom.packages.isPackageActive('tree-view')).toBe(true)
})
})
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function() {
it("should be opened in an editor", function() {
waitsForPromise({ shouldReject: false, timeout: 5000, label: 'promise to be resolved or rejected' }, () =>
atom.workspace.open('c.coffee').then(editor => expect(editor.getPath()).toContain('c.coffee'))
)
})
})
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function() {
it("is async", function() {
const spy = jasmine.createSpy('fs.readdirSpy')
fs.readdir('/tmp/example', spy)
waitsFor(() => spy.callCount > 0)
runs(function() {
const exp = [null, ['example.coffee']]
expect(spy.mostRecentCall.args).toEqual(exp)
expect(spy).toHaveBeenCalledWith(null, ['example.coffee'])
})
})
})
For a more detailed documentation on asynchronous tests please visit the Jasmine documentation.
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Atom core specs when working on Atom itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function() {
fit("has some expectations that should pass", function() {
expect("apples").toEqual("apples")
expect("oranges").not.toEqual("apples")
})
})
It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the Travis CI For Your Packages and AppVeyor CI For Your Packages posts for more details.
To run tests on the command line, run Atom with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
atom --test --timeout 60 ./test/test-1.js ./test/test-2.js
Warning: This API is available as of 1.2.0-beta0, and it is experimental and subject to change. Test runner authors should be prepared to test their code against future beta releases until it stabilizes.
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Atom will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Atom will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters:
applicationDelegate
An object responsible for Atom's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.atom
).enablePersistence
A boolean indicating whether the Atom environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via atom --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finish running. This exit code will be returned when running your tests via the command line.