Feed Your Grails Karma (#ggx 2014)
-
Upload
vladimir-orany -
Category
Software
-
view
500 -
download
3
Transcript of Feed Your Grails Karma (#ggx 2014)
Feed Your Grails KarmaVladimír Oraný
Overview
Test Driven Backend REST Application (Grails, Spock)
Test Driven Frontend Single Page Application (AngularJS, Jasmine,
Karma, Geb)
Continuous Integration capable Test Pipeline (Travis CI)
Grails
Rapid web development framework
Plays well with Test Driven Development …
… as long as you stick with GSPs
Great REST support since 2.3
Geb
Browser automation solution
jQuery-like selectors
plays well with Grails and Spock
great tool for functional tests
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
Model Catalogue Plugin
Grails PLugin for managing any
metadata
REST API Backend
AngularJS services and directives
https://github.com/MetadataRegistry/
ModelCataloguePlugin
“A single-page application (SPA), is a web application or web site
that fits on a single web page with the goal of providing a more
fluid user experience akin to a desktop application.”
–The Poeple of the Internet (aka Wikipedia)
Single Page Application
Ancient Architecture
ServerFat
Client
Shared Libararies
SPA Architecture
REST
backend
Rich
Client
REST API (JSON)
SPA
Sample Application(s)
Earl’s List
Backend application in Grails
Frontend application in AngularJS
with little help of Grails (Asset Pipeline)
https://github.com/musketyr/earls-list
Backend Application
Grails REST application
grails create-app earls-list
cd earls-list
grails create-domain-class org.example.todo.Item
grails create-controller org.example.todo.Item
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
@Unroll void “item from #params has #errorCount errors"() {when:Item item = new Item(params).save(failOnError: true)
then:item.errors.errorCount == errorCount
where:errorCount | params1 | [description: '']0 | [description: 'Do IT']1 | [description: 'x' * 256]
}
class Item {String descriptionBoolean crossed = Boolean.FALSE
static constraints = {description size: 1..255
}}
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
class ItemControllerSpec extends IntegrationSpec {
void "create new item"() {when:
controller.response.format = 'json' controller.request.json =[description: 'Do IT!']controller.request.method = 'POST'controller.save()
def result = controller.response.json
then:result.description == 'Do IT'
}
class ItemController extends RestfulController<Item> {
def responseFormats = ['json']
ItemController() {super(Item)
}
}
@TestFor(UrlMappings) @Mock(ItemController)class UrlMappingSpec extends Specification {
void "test item rest endpoints ready"() {expect:assertRestForwardUrlMapping(method, url,
controller: "item", action: action, paramsToCheck)
where:method | action | url | paramsToCheck"GET" | "index" | "/item/" | {}"POST" | "save" | "/item/" | {}"PUT" | "update" | "/item/1" | { id = "1" }"DELETE" | "delete" | "/item/1" | { id = "1" }
}
class UrlMappings {
static mappings = {"/item/" controller: 'item', action: 'index',
method: HttpMethod.GET"/item/" controller: 'item', action: 'save',
method: HttpMethod.POST"/item/$id" controller: 'item', action: 'update',
method: HttpMethod.PUT"/item/$id" controller: 'item', action: ‘delete',
method: HttpMethod.DELETE}
}
Frontend Application
AngularJS for controllers, services and templates
dependency injection => easier testing
Bootstrap for theming
Jasmine and Karma for testing
SPA Architecture
REST
backend
Rich
Client
REST API (JSON)
JsonRecorder
Records the JSON i/o in the controller tests
repositories {mavenRepo "http://dl.bintray.com/metadata/model-catalogue"
}
dependencies {test "org.modelcatalogue:json-recorder:0.1.0"
}
JsonRecorder
def rec = JsonRecorder.create('test/js-fixtures/earlslist', 'item')
void "obtain the list of items in json format"() {when:controller.index(10)
def result = rec << 'list' << controller.response.json
then:result?.size() == 10result[0].description =~ /Item #\d+/
}
(function (window) {window['fixtures'] = window['fixtures'] || {};var fixtures = window['fixtures'];fixtures['item'] = fixtures['item'] || {};var item = fixtures['item'];window.fixtures.item.list = [
{"id": 17,"description": "Item #16","class": "org.example.todo.Item","crossed": false,"dateCreated": "2014-12-07T04:56:44Z"
},...
];})(window);
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
Where to find frontend tools and libraries?
This slide was intentionally left blank …
Front-end Developer Toolbox
NodeJS
Platform for creating server application in JavaScript
Platform for creating/sharing tools for frontend developers
Package manager NPM
npm install
Plays well with CI servers
package.json
{"name": "earls-list","description": "Sample TODO List","version": "0.0.1","devDependencies": {
"bower": "~1.3.12","karma": "~0.12.0","karma-junit-reporter": "~0.2.1","karma-jasmine": "~0.2.0","karma-firefox-launcher": "~0.1.3"
}}
Bower
Package manager for frontend application
Based on Git
bower.json with similar syntax as package.json (NPM)
bower install
bower.json
{"name": "earls-list","description": "Sample TODO List","version": "0.0.1","dependencies": {"angular": "~1.3.5","bootstrap": "~3.3.1"
},"devDependencies": {"jasmine": "~2.1.3","angular-mocks": "~1.3.5"
}}
.bowerrc
{"directory": "grails-app/assets/bower_components"
}
Asset Pipeline Plugin
//= require jquery/dist/jquery//= require angular/angular//= require bootstrap/dist/js/bootstrap
//= require items//= require itemsCtrl
angular.module('earlslist', ['earlslist.items','earlslist.itemsCtrl'
]);
Asset Pipeline Plugin
<!DOCTYPE html><html><head><title>Earl's List</title><asset:stylesheet src="earlslist/items.css"/><asset:javascript src="earlslist/index.js"/><script type="text/javascript">angular.module('earlslist.apiRoot', []).value('apiRoot', '${request.contextPath ?: ''}');
</script></head>
Jasmine
Behaviour-Driven JavaScript
Assertions with matchers
Mocking and Spying
describe('Items service wraps the backend', function(){// setupbeforeEach(module('earlslist.items'));
it('should list all items', inject(function(…){// feature method
}));
...}
it('should list all items', inject(function(items, $httpBackend){var list = null;
$httpBackend.expectGET(‘/item/‘).respond(angular.copy(fixtures.item.list));
items.list().then(function(fetched){ list = fetched; });
expect(list).toBeNull();
$httpBackend.flush();
expect(list).toBeDefined();expect(list.length).toBe(10);
}));
it('should create new item', inject(function(items, $httpBackend){var item = null;
$httpBackend.expectPOST('/item/', fixtures.item.validSaveInput).respond(angular.copy(fixtures.item.validSave));
items.save(fixtures.item.validSaveInput).then(function(created){item = created;
});
expect(item).toBeNull();
$httpBackend.flush();
expect(item).toBeDefined();expect(item.id).toBe(35);
}));
angular.module(‘earlslist.items', function($http, apiRoot){
var items, unwrapOrReject, enhanceAll, enhanceItem;
unwrapOrReject = function(response){…};
enhanceItem = function(item) {…};
enhanceAll = function(listOfItems) {…};
return {
list: function(start) {
return $http({
url: apiRoot + "/item/",
method: 'GET',
params: start ? {offset: start } : {}
}).then(unwrapOrReject).then(enhanceAll);
},
...
};
});
Karma
Test runner for JavaScript tests
Can run multiple browsers including HtmlUnit and PhantomJS
karma start karma.conf.json
module.exports = function(config) {config.set({
...files: [
'grails-app/assets/bower_components/jquery/dist/jquery.js','grails-app/assets/bower_components/angular/angular.js','grails-app/assets/javascripts/**/*.js','grails-app/assets/bower_components/angular-mocks/angular-mocks.js','test/js-fixtures/**/*.js','test/js/**/*.js'
],...
});};
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
<body ng-app="earlslist"><div class="container"><div ng-include="'earlslist/items.html'"></div>
</div></body>
ctrModule.run([‘$templateCache', function($templateCache){
$templateCache.put('earlslist/items.html',
'<div class="row" ng-controller="earlslist.itemsCtrl">' +
...
'</div>');
}]);
<form ng-submit="addItem()" class="col-md-12 form"><div class="alert alert-danger" ng-repeat="error in errors">{{error.message}}
</div><input ng-model="newItemText" ng-disabled="loading">
</form><h2 ng-repeat="item in items"><span ng-click="toggle(item)"></span><span class=“item-description" ng-click=“toggle(item)”>
{{item.description}}</span><span class="pull-right" ng-click="remove(item)">Delete</span>
</h2>
angular.module('earlslist.itemsCtrl', ['earlslist.items']).controller('earlslist.itemsCtrl', function($scope, items){...$scope.addItem = function() {
items.save({description: $scope.newItemText}).then(function(newItem){
$scope.newItemText = '';$scope.items.unshift(newItem);$scope.errors = [];
}).catch(function(response){$scope.errors = response.data.errors;
});};
});
beforeEach(inject(function ($rootScope, $controller, _items_, $q) {items = _items_;$scope = $rootScope.$new();
// returns 10 items for first call and 3 for secondspyOn(items, 'list').and.callFake(function () {…});
$controller('earlslist.itemsCtrl', {$scope: $scope, items: items
});}));
it('should list all items', function(){expect($scope.errors.length).toBe(0);expect($scope.items.length).toBe(0);expect($scope.loading).toBe(true);
$scope.$digest();
expect($scope.errors.length).toBe(0);expect($scope.items.length).toBe(13);expect($scope.loading).toBe(false);
});
it('renders 13 items', inject(function($templateCache, $compile) {var tpl, element;
tpl = $compile($templateCache.get(‘earlslist/items.html'))element = tpl($scope);
$scope.$digest();
expect(element.find('.item').length).toBe(13);
}));
GSP vs. SPA
Domains
Domains
GSP
SPA
Serv + Ctrl
S + C + REST
Taglibs
Components
GSP
Not Covered
DB Services View Support Views
Func
Func
App
Covered
Templates
Legend
Functional tests with Geb
reportsDir = new File("target/geb-reports")reportOnTestFailureOnly = falsebaseUrl = 'http://localhost:8080/earls-list/'
driver = { new FirefoxDriver() }
waiting {timeout = 15retryInterval = 0.6
}
// Default to wraping `at SomePage` declarations in `waitFor` closuresatCheckWaiting = true
class HomePage extends Page {static url = "#/"static at = { heading.text() == "Earl's List" }static content = {
heading { $("h1") }task { $("#task") }items(required: false) { $(".item") }itemsTexts(required: false) {
items.find('.item-description') }errors(required: false) { $(".alert-danger") }closeError(required: false) { $(".close") }
}}
def "go to home page and enter new task"() {when:
to HomePagethen:
at HomePageexpect:
!items.size()when:
task = 'Test with Geb'task << Keys.ENTER
then:waitFor {
items.size() == 1}
}
„Will it play on continuous integration server?“
Travis CI
free for public GitHub repostories
configuration stored in the repository
NodeJS available for every build
Firefox and Chrome for headless testing
.travis.yml
language: groovyjdk:- oraclejdk7before_install:- "npm install"- "bower install"- "export DISPLAY=:99.0"- "sh -e /etc/init.d/xvfb start"
script:- "./grailsw refresh-dependencies"- "./grailsw test-app"- "./node_modules/karma/bin/karma start --single-run --browsers Firefox"
Summary
Grails SPAs are actually two applications
Use the front-end developers tools for front-end app
Use your backend tests to generate the fixtures
https://github.com/musketyr/earls-list
Thank you
Vladimír Oraný
@musketyr