Jasmine & ExtJS' MVC: A Love Story
— Comments — ajax, bdd, code, extjs, html, jasmine, java, javascript, selenium, sencha, tdd, tech, testing, unit testing, user interface, web — 4 min read
I've been looking for a way to perform unit tests on my new project's UI code. The projects I worked on before were practically prepackaged and handed to me — Java has a pretty mature testing and build suite: JUnit and Maven. My current project is 99% JavaScript, and there isn't a defacto test suite for that. Googling around has led me to several testing tools, such as Selenium and JSpec. I even began digging deeper into some of them, but then I discovered Jasmine, and that Sencha developers use Jasmine. It was a sign.
So, with Jasmine downloaded, I began playing with it and trying its examples. I loved it. Pure JavaScript solution with no external dependencies; I could simply hit the test page and off the tests go. I'm sure with a little more digging it would work in a offline/build/cli environment with PhantomJS or node.js. The only hurdle I had left was how (and what) I wanted to test. Not every file/class in this UI stand alone, or the application state. How would I want to simulate the server? So many things to sort out before I was satisfied that I had a solution.
Then I had an idea. What if the tests ran under their own version of Ext.Application? My main app.js file was simple: it defined the "Application", its namespace, its path, the controllers and the launch method, which, for me, only initiated the login sequence. Basically, my idea was to copy the app.js to app-test.js, then copy index.html to test.html (this application is using the ExtJS4 MVC project layout).
So, here's my folder structure. It will look a little familiar as I've based this example on the documentation's example layout.
So, we have the jasmine package under app-test/lib, and mock data under app-test/mock and all the test suites ("specs") under app-test/specs.
index.html and test.html look very similar, except that test doesn't include the loading markup and loads the jasmine scripts and app-test.js instead of app.js.
Those familiar with either ExtJS or Jasmine can probably see where I'm going with this. Jasmine doesn't start automatically. It has a bootstrap method that you call to run the tests and show the output. The Jasmine examples always show the test-runner.html with a script block in the body that runs as soon as the browser hits that block. This isn't what we want for this application. Not all of the resources and code will be loaded, at least not consistently at this point. So we have the app-test.js define the Application in "test mode." Maybe a visual is better in this case.
This is the main entry point and the app.js.
<html> <head> <title>App</title> <link rel="shortcut icon" href="resources/favicon.ico" /> <link rel="stylesheet" type="text/css" href="extjs/resources/css/ext-all-gray.css" /> <link rel="stylesheet" type="text/css" href="resources/css/main.css" /> </head> <body> <div id="loading-mask"></div> <div id="loading"> <div class="loading-indicator">Loading...</div> </div>
<script type="text/javascript" src="extjs/ext-debug.js"></script> <script type="text/javascript" src="app.js"></script> </body></html>
Ext.application({ name: 'App', appFolder: 'app',
controllers: [ 'Session', /*... all your controllers here... * they will include your views, models, etc */ ],
launch: function () { setTimeout(function clearMask() { Ext.get('loading').remove(); Ext.get('loading-mask').fadeOut({ remove: true }); resizeBlocker(Ext.Element.getViewWidth()); }, 100);
/* This is this application's kickstart... this shows a login * form if not logged in and opens the Viewport once logged in. * If a login/session is detected on reload, it will just open * the viewport. */ App.controller.Session.login(); },});
Now this is what goes into test.html and app-test.js:
<!--This is a Jasmine test runner. please see the wiki on how to write tests.https://github.com/pivotal/jasmine/wiki--><html> <head> <title>App - Jasmine Test Runner</title> <link rel="shortcut icon" href="resources/favicon.ico" /> <link rel="stylesheet" type="text/css" href="app-test/lib/jasmine-1.1.0/jasmine.css" />
<!-- all your framework code here --> <script type="text/javascript" src="extjs/ext-debug.js"></script>
<!-- Jasmin code here --> <script type="text/javascript" src="app-test/lib/jasmine-1.1.0/jasmine.js"></script> <script type="text/javascript" src="app-test/lib/jasmine-1.1.0/jasmine-html.js"></script>
<!-- include specs here --> <script type="text/javascript" src="app-test/specs/example.spec.js"></script>
<!-- test launcher --> <script type="text/javascript" src="app-test.js"></script> </head> <body></body></html>
/* * Define mock framework objects here */var AppConfig = { host: 'test:' };
var io = {}; //socket.io mock?...
Ext.application({ name: 'App', appFolder: 'app',
controllers: [ 'Session', /*... all your controllers here... * they will include your views, models, etc */ ],
launch: function () { hookAjax();
//include the tests in the test.html head jasmine.getEnv().addReporter(new jasmine.TrivialReporter()); jasmine.getEnv().execute(); },});
Next, what about those pesky server calls? Lets reroute those to our mock directory. In my application all Ajax calls use a url in a config for the host. This allows us to change the host without deploying to a new server. So, because of this, its super easy to detect a server call: its prefixed with the config value of the host and I've set the mock config object above to have the host set to "test:", so here is my hook:
function hookAjax() { Ext.Ajax.request_forReal = Ext.Ajax.request; Ext.Ajax.request = function test_ajax(o) { if (/^test:/i.test(o.url)) { o.url = o.url.replace(/^test:/i, './app-test/mock'); } this.request_forReal.apply(this, arguments); };}
In summation, what does this get us? We now have a way to isolate anything we want in the application and test the heck out of it. We can test integration of components/controllers/models, or individual components.
Bam. mike drop