Rapid Prototyping x AngularJS at Google Brussels HQ
We like to think outside of the box, and when "La Feweb" asked us to give a technical talk about AngularJS, we immediatly went crazy !
AngularJS's dependency injection architecture
We proposed to give a presentation about the dependency injection system of AngularJS and the given opportunity to mock REST communications in order to master the behaviour of a web application before wiring it to a production environement. This software design pattern allows anyone to leverage the power and flexibility of a decoupled architecture, especially when it comes to be able to test at different levels (Unit testing, UI testing and E2E testing).
Why is it an important matter ?
If you want to understand more in depth this pattern, we suggest you to read Dependency Injection at Wikipedia. The short answer is : it helps developers to produce maintainable code and greatly increase the quality of an application while reducing the chances to have to fire-fight an incident in production.
Let's go crazy !
Maurizio Pedriale, Benoit Toussaint and I all participated to this project. Benoit and I decided to go crazy and started to think about the creative way to demonstrate why it is important to test properly an application before going into mission critical production environment where everything can blow-up if not properly controlled.
As always it started with a discussion and I sketched the early ideas !
"What about a connected banner and a firework (electric air compressed confeti canon), the demo application will be coded live and will act as a remote controler to fire all the things !"
For sure the picture looked amazing in our imagination and we thought that was a fun way to entertain people during an nice event afterwork while still remain serious about software engineering practices. The tone was set, we had to demontrate the proper way to test and connect a demo application to a critical component (the banner and the firework must be triggered the right way at the right moment and give back the proper result to the remote in order to update the UI).
To be honest it was easier said (or sketched) than done and the road the make it concrete and working in the real world was a bit of a challenge !
To validate your most critical hypothesis first, you have. Yes, hmmm.
It is true that it was the very first time we decided to build such a big banner that should be WiFi enabled, exposing a REST api and being able to be deployed in an unknown environement also driving the circuitry to fire up an electric air compressed confeti canon.
And such a goal comes with a lot of uncertainty. Our immediate response in such situation is to quickly build a prototype to validate the first hypothesis about how everything should integrate together, let's call it our first integration test !
Before spending too much money for a colorfull rollup tailored for the best propaganda, it was absolutely necessary to try out something a little bit less fancy, a rollup window store, some pine timber, recycled cardboard and a bit of super-glue.
Re-engineering and new hypotheses validation
It was time to consolidate the learning and start building a much stronger and better designed version (with the help of the kids :) including the wiring to the air compressed canon.
The second version of the structure was built, the aREST arduino library used to publish a REST api for the Arduino UNO R3 fully loaded with a new super power thanks to the official Arduino WiFi shield.
The arduino exposed one endpoint through a dedicated Wifi network in order to trigger a servo motor used the control the opening of the banner's lid.
If you want to light-up the room with sparkling paper confetis, you should probably try out first with spare canon in order to understand its inner mechanic and make sure another less critical hypothesis (but important) would not go wild without any validation red-hot stamp.
An air compressed confeti canon is quite simple, the steel cap maintaining the pressure under a rubber joint is maintained by a single "synthetic" string. When the proper voltage is applied, the string melts and the air-blow create the magic moment.
We had to rethink and simulate again the schematic to make sure everything was working properly under 18v instead of 9v.
The AngularJS demo application
The UI of the demo application is quite simple (and mobile responsive), the app display the status online/offline of the connected banner (the server) and offers a button to fire the all things after which the user gets a notification that everything went as expected (the banner could be out of sight, who knows ;).
The structure of the app
The code of the application is hosted on GitHub and the structure it follows is called a Single-page application (SPA).
The file index.html
is bound to the main module fireworksRC
defined in the script app.js
and declares an element <div ng-view></div>
hosting the rendered template associated with routes managed by the $route
service.
<!DOCTYPE html>
<html lang="en" ng-app="fireworksRC" class="no-js">
...
<body>
<div ng-view></div>
...
<script src="app.js"></script>
<script src="home/home.js"></script>
</body>
</html>
There is only one route, the home
route is activated when the user navigates to http://webserver.local/ or http://webserver.local/#/home which is rendered dynamically within the ngView
directive.
Replace webserver.local
by the address of the machine serving the files (run npm start
). For the production version, checkout the final-version
branch and dev-version
for the development one.
The top level module fireworksRC
contains only a configuration for the default route to be forwarded to the home
route. The module definition include the required dependencies (ngRoute
, fireworksRC.home
) injected at run time by the dependency injector.
angular.module('fireworksRC', [
'ngRoute',
'fireworksRC.home'
]).
config(['$routeProvider', function($routeProvider) {
$routeProvider.otherwise({redirectTo: '/home'});
}]);
This is the only responsability of the main module, everything else is delegated to another module fireworksRC.home
defined in the script home/home.js
The fireworksRC.home
module
angular.module('fireworksRC.home', ['ngRoute'])
.constant('FireworksBE_URL', 'http://192.168.1.100')
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/home', {
templateUrl: 'home/home.html',
controllerAs : 'home',
controller: 'HomeCtrl'
});
}])
.controller('HomeCtrl', ['FireworksBE_URL','$http', function(FireworksBE_URL, $http) {
...
}]);
The module defines a constant
, a config
and a controller
. The constant FireworksBE_URL
is used to locate the address of the connected banner exposing REST endpoints a.k.a. the server.
The config
instructs the provider $routeProvider
to render the html template located at home/home.html
and associate the controller HomeCtrl
to the view as home
.
Finally, the declaration of the controller named HomeCtrl
contains the required dependencies, the constant FireworksBE_URL
and the core AngularJS service $http
which again will injected at run time by the dependency injector.
The $http
service
This is the service we are going to use at the controller level to interract with the connected banner exposing a REST api.
The $http service is a core AngularJS service that facilitates communication with the remote HTTP servers via the browser's XMLHttpRequest object or via JSONP.
When a system is designed to be tested ...
Then obviously it is easier to test it ! The very first reflex when it comes to build a system is cooking some unit tests ! We use Karma to test our main controller HomeCtrl
.
In this case we use Jasmine to describe the unit test suite of the fireworksRC.home
module which mainly target the HomeCtrl
controller.
Here are the main behaviours of the HomeCtrl
:
- It should detect if the remote system is unreachable
- It should detect if the remote system is available
- It should not fire if already launched
- It should fire if not already launched
Here is the main structure of the test suite located in the file home/home_test.js
describe('fireworksRC.home module', function() {
describe('An application controller', function(){
it('should detect if the remote system is unreachable', function() {
});
it('should detect if the remote system is available', function() {
});
it('should not fire if already launched', function () {
});
it('should fire if not already launched', function() {
});
});
});
$httpBackend, the real, the fake and the supercharged fake!
As mentionned earlier in this article, we chose $http
service provided in the AngularJS's module ng
as a way to communicate easily with the connected banner exposing the REST api.
The real one
This service internally depends on ng.$httpBackend
that delegates to XMLHttpRequest object or JSONP and deals with browser incompatibilities.
The fake one
When it comes to unit test the controller in order to asses its behaviour, the dependency injector is essential. We are going to use it to inject a fake version of the $httpBackend
service provided by AngularJS itself within the module ngMock
.
When an AngularJS application needs some data from a server, it calls the $http service, which sends the request to a real server using $httpBackend service. With dependency injection, it is easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify the requests and respond with some testing data without sending a request to a real server.
The module ngMock
is included in angular-mocks.js
. This dependency is managed via Bower which install the required script in app\bower_components\angular-mocks
as configured in the bower configuration file .bowerrc
along with bower.json
.
The module ngMock
must be provided to the test suite runner in order to be properly used. This is done by referencing this dependency app\bower_components\angular-mocks\angular-mocks.js
in the runner configuration file karma.conf.js
.
module.exports = function(config){
config.set({
basePath : './',
files : [
'app/bower_components/angular/angular.js',
'app/bower_components/angular-route/angular-route.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/home/**/*.js'
],
...
The supercharged fake
There is a second mock version of the $httpBackend
service located under the module ngMockE2E
. This one is particulary useful for integration tests where you want to simulate only certain responses.
This implementation can be used to respond with static or dynamic responses via the when api and its shortcuts (whenGET, whenPOST, etc) and optionally pass through requests to the real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch templates from a webserver).
This strategy proove to be useful when combined with an automated continous integration pipeline where you inject a new module that overrides the main module for example. This injection has nothing to do with a dependency injection but is more a text replacement within the files themselves.
You can find an example of this one in the dev-version
branch. The index.html now use fireworksRC_DEV
as the main module.
...
<html lang="en" ng-app="fireworksRC_DEV" class="no-js">
...
<body>
...
<script src="bower_components/angular-mocks/angular-mocks.js"></script>
...
</body>
</html>
This one overrides the fireworksRC
module in app.js
and this time uses properly the module ngMockE2E
located in angular-mocks.js
properly referenced in index.html
.
...
angular.module('fireworksRC_DEV', [
'fireworksRC',
'ngMockE2E'
]).
run(['$httpBackend', function($httpBackend) {
$httpBackend.whenGET('http://192.168.1.100').respond({
"id": "001",
"name": "fireworks_system",
"connected": true
});
$httpBackend.whenGET('http://192.168.1.100/fire?params=0').respond({
"return_value" : 1});
$httpBackend.whenGET('home/home.html').passThrough();
}]);
In this case, the connected banner will always seem available to the application and the firing will always result in a correct return value.
Deliberately using the injector
Let's flesh out the first unit test in home\home_test.js
. We first define some variables that will host what we need to properly test our controller HomeCtrl
within its module fireworksRC.home
.
describe('fireworksRC.home module', function() {
var SIMULATOR_URL = 'http://simulator.local';
var VALID_HTTP_GET = {
"id": "001",
"name": "fireworks_system",
"connected": true
};
...
We will use SIMULATOR_URL
to force the value of the constant FireworksBE_URL
defined in the main module fireworksRC
in app.js
. We deliberately exluded the file app.js
from our test runner configuration (karma.conf.js
) but we need this constant to be available in the scope of the controller HomeCtrl
that we are going to instantiate in the test suite.
VALID_HTTP_GET
will be used as a response when the controller HomeCtrl
will call the connected banner (the server) to check its availability by issuing a GET request.
Jasmine provides the function beforeEach(function)
that is executed before each spec in the describe
in which it is called. So we use it to set the value of the constant FireworksBE_URL
to the one of SIMULATOR_URL
.
describe('fireworksRC.home module', function() {
var SIMULATOR_URL = 'http://simulator.local';
var VALID_HTTP_GET = {
"id": "001",
"name": "fireworks_system",
"connected": true
};
beforeEach(module('fireworksRC.home',function($provide) {
$provide.constant('FireworksBE_URL', SIMULATOR_URL);
}));
...
We describe a new spec for the controller. This time we use the inject()
function defined in the ngMock
module to deliberately inject the required elements, the $httpBackend
mock and the $controller
service in order to instanciate our controller.
...
describe('An application controller', function(){
var ctrl, $httpBackend;
beforeEach(inject(function(_$httpBackend_, $controller) {
// init $httpBackend
$httpBackend = _$httpBackend_;
// init whenGET normal VALID_HTTP_GET
$httpBackend.whenGET(SIMULATOR_URL).respond(VALID_HTTP_GET);
$httpBackend.whenGET(SIMULATOR_URL + '/fire/params=0').respond(VALID_FIRE_RESPONSE);
ctrl = $controller('HomeCtrl');
}));
...
The dependency injector is able to wrap the underscore around the required dependency in order to let us properly define the variable $httpBackend
that is that way properly declared in the accessible scope of the controller and will host the ngMock.$httpBackend
service. Remember $http
uses $httpBackend
as a dependency.
We hook up the GET requests to simulate the responses and we instanciate the controller.
The test setup is done, we can now write the first unit test that will evaluate the controller's behaviour.
it('should detect if the remote system is available', function() {
$httpBackend.expectGET(SIMULATOR_URL);
$httpBackend.flush();
expect(ctrl.connected).toBe(true);
});
We expect the controller to initiate a request when it is instanciated in order to retreive the status of the server and update accordingly its property connected
.
If you execute that test (npm test
) it will fail until you provide the right code within the controller in home/home.js
.
...
.controller('HomeCtrl', ['FireworksBE_URL','$http', function(FireworksBE_URL, $http) {
var that = this;
that.connected = false;
var updateSystemData = function() {
$http.get(FireworksBE_URL).
success(function(data, status, headers, config) {
if(status === 200 && data.connected === true) {
that.connected = true;
}
}).
error(function(data, status, headers, config) {
});
};
updateSystemData();
}]);
Conclusion
AngularJS is a very nice and well designed framework, it helps to create production ready application. On the other hand, the broad spectrum of IoT solutions allows you to create almost anything without being an expert in hardware.
Rapid prototyping implies to be able to see the end-result and design the right tests to validate your most critical hypotheses first.
We hope you enjoyed the show !
Alan