#Sparrow functional testing
A functional website testing framework based on jasmine and JQuery. Sparrow is designed to solve the problem of async testing inherent in website testing by making async testing easier.
Sparrow:
- Makes async testing easier
- Allows testing multiple pages simultaneously so complex interactions can be tested
- Runs tests in a browser and headless
- Allows using the IntelliJ/Webstorm debugger with both test and page code
- Uses JQuery and Jasmine to reduce the learning curve
- Makes it easier to create independent tests that are not dependent on each other
##Quick Start
-
Download the latest sparrow
-
cd to installation directory
-
type 'npm install' (requires node)
-
type 'grunt headed' to create specRunner.html (requires grunt)
NOTE: Make sure you run this command after creating new spec/helper files and after upgrading Sparrow. You can also rungrunt watchto build specRunner.html automatically when spec/helper files change. -
open runner.html in a browser (tested with newest Chrome and FF)
(NOTE: FF seems to allow running from the local filesystem. Chrome requires running it from a web server) -
Look at the files in /specs to see some of the possibilities
##Running a single test or a group of tests
To run a single test or a group of tests, simply add ?spec=some%20it%20or%20description to the end of the url the same way you would running Jamsine directly.
example:
to run 'should do something' use ?spec=should%20do%20something at the end of the url.
##Running tests in headless mode
To run tests in headless/CI mode. type 'grunt' or 'grunt headless'
If you need to run the tests from a server with dynamic content, simply add the host address in Gruntfile.js. It is best to run the tests from the source directory the server is pointed to or a symlink.
##Sparrow options
####sparrow.WAIT_TIME The time to wait for the waitFor*, waitUntil*, waitWhile*
This can be set in a helper to make it a global setting for all tests
##Basics
There are two ways to use the sparrow functions. You can call them directly and provide a callback for the async functions, or you can do things the easier way and use the sparrow async() monad.
Callback example
describe('block of tests', function() { it('should do something', function(done) { createTestWindow('win'); // open the page $win.open('http://example.com', function() { $win.click('#some-link'); // fill the form in the popup $win.waitForSelector('#some-popup', function() { $win.fill('#some-form', {name:'me', ...}; $win.click('#sumit-button'); // Do something with the resulting page $win.waitforText('new page loaded', function() { $win.click('#another-link'); $win.waitWhileVisible('#some-modal', function() { myAsyncFunction(function() { // Test that we see 'finished' expect($win.find('#result').html()).toBe('finished'); done(); }); }); }); }); }); }); });
Async monad example (much cleaner and easier to refactor)
describe('block of tests', function() { it('should do something', function(done) { createTestWindow('win'); $win.async(done) // open the page .open('http://example.com') .click('#some-link') // fill the form in the popup .waitForSelector('#some-popup') .fill('#some-form', {name: 'me', ...} .click('#submit-button') // Do something with the resulting page .waitForText('new page loaded') .click('#another-link') .waitWhileVisible('#some-modal') .fn(myAsyncFunction) // Test that we see 'finished' .syncFn(function() { expect($win.find('#result').html()).toBe('finished') }) .run(); }); function myAsyncFunction(done) { // do something async done(); } });
Sparrow is based on jasmine so tests are written in the same style. See jasmine docs for more possibilities.
##Using with Meteor
Unpack sparrow someplace in the /public directory. I put it in /public/sparrow. Sparrow tests will then be available at http://localhost:3000/sparrow/runner.html.
NOTE: I have found that I must clear the cache in my browser to get updates after changing specs.
##Documentation
NOTE: If you are using the async monad, ignore the doneCB argument on the following, this is handled for you.
- .run()
- .open(url, doneCB)
- .show()
- .waitUntilTrue(testFn, doneCB)
- .waitForText(text, doneCB)
- .waitForSelector(selector, doneCB)
- .waitUntilVisible(selector, doneCB)
- .waitWhileVisible(selector, doneCB)
- .wait(ms, doneCB)
- .click(selector)
- .log(message)
- .fill(selector, formData)
- .close()
- .fn(someFunction)
- .syncFn(someFunction)
- .extend(obj)
- .http.post(url, data, success, done)
Opens a test window tab and creates $name variable in the test scope.
it('should do something', function() { createTestWindow('aTestWindow'); ... creates $aTestWindow });
####.async(doneCB)
Creates an async monad to make async functions easier
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a#link') .waitForText('something on new page') .run() });
####.run()
Starts a previously created async monad
it('should do something', function(done) { createTestWindow('win'); $win.async(done) // other instructions here .run(); });
Opens a url in a window tab.
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .open('http://google.com' // other instructions here .run() });
####.show()
Show the tab for this window
it('should do something', function(done) { createTestWindow('win'); createTestWindow('another'); $win.async(done) .show() // This will show the $win window (tab) .run() $another.async(done) .show() // This will show the $another window (tab) .run() });
####.waitUntilTrue(testFn, doneCB)
Wait until the test function returns true
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('#something') .waitUntilTrue(somethingHappening) }); function somethingHappening() { return $j('#xxxx').html() === 'ready' }
waitUntilTrue can also takes an async function containing a "done" final argument.
The passed async function will be called until it calls done() with a truthy value or sparrow.WAIT_TIME is reached.
it('should do something', function(done) { var count = 0; $waitFor.async(done) .waitUntilTrue(asyncFunc) .run(); function asyncFunc(done) { setTimeout(function() { ++count === 2 ? done(1) : done(0); },1); } }) });
####.waitForText(text, doneCB)
Wait until the text is visible on the webpage
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .waitForText('some text on the page') .run() });
####.waitForSelector(selector, doneCB)
Wait until the selected element is in the dom
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .waitForSelector('a#my-link') .run() });
####.waitUntilVisible(selector, doneCB)
Wait until the selected element is visible
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .waitUntilVisible('#myAsyncModal') .run() });
####.waitWhileVisible(selector, doneCB)
Wait while the selected element is visible
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .waitWhileVisible('#loadingMessage') .run() });
Wait for some period of milliseconds
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .wait(2000) .click('#something else') .run() });
####.click(selector)
Click on some selected element
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .wait(2000) .click('#something else') .run() });
####.log(message)
Send a log message to the console.
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .log('I clicked it') .run() });
Fill in the selected form
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .fill('#my-form', {name: 'Scott', email:'me@mine.com'}) .click('#submit') .run() });
####.close()
Close an open test window (tab)
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .fill('#my-form', {name: 'Scott', email:'me@mine.com'}) .click('#submit') .close() .run() });
Call an async function
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .fn(myAsyncFunction) .run() }); function myAsyncFunction(done) { // do something done(); }
Call a function. Currently this is the best way to add expects into your test code.
it('should do something', function(done) { createTestWindow('win'); $win.async(done) .click('a') .syncFn(function() { expect($win.find('#something').html()).toBe('my content'); }) .run() });
Add functions to sparrow. The first argument will be the test window variable. For example, if you created a test window called "myWin", then called $myWin.write(). winVar would be $myWin.
NOTE: To add an async function, the final argument must be named 'done'
sparrow.extend({ write: function(winVar, selector, content) { winVar.find(selector).append(content); }, waitForTesting: function(winVar, done) { winVar.waitForText('testing', done); } }); ` ####<a name="httpPost">.http.post(url, data, success, done) ```javascript $tests.async(done) .fn(function(done) { $tests.http.post('/some/url', {some:'data'}, _.partial(checkReturn, done)); }) .run(); function checkReturn(done, ret) { expect(ret).toBe('<some> <html>'); done(); }
##Troubleshooting
Accessing the window object in a tab
You can use the $window variable to access the window object inside of a tab from the debugging console.
For example, if your tab is $page, then $page.$window will give you the window variable within that tab.
From there you can access any global variables.
###Debugging in the middle of an async monad chain
The easiest way to debug in the middle of the async chain is to use syncFn. Add a temporary .syncFn() call and put your debug code inside of the passed function. The same technique works for setting breakpoints.
$win.async(done) .click('#something') .waitFor('#something-else') .syncFn(function() { console.log($win.find('#some-thing').html()) }) .run()
Feedback
If you find Sparrow useful, please let me know. If you have any questions or concerns, please feel free to let me know at scott@bulldoginfo.com.
I created Sparrow because none of the other testing frameworks I found had the features I was looking for. I have found this framework very useful for testing production websites.
If you are looking for someone to setup functional testing for your web application or website, contact me.