Feed Your Grails Karma (#ggx 2014)

70
Feed Your Grails Karma Vladimír Oraný

Transcript of Feed Your Grails Karma (#ggx 2014)

Page 1: Feed Your Grails Karma (#ggx 2014)

Feed Your Grails KarmaVladimír Oraný

Page 2: Feed Your Grails Karma (#ggx 2014)

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)

Page 3: Feed Your Grails Karma (#ggx 2014)

Grails

Rapid web development framework

Plays well with Test Driven Development …

… as long as you stick with GSPs

Great REST support since 2.3

Page 4: Feed Your Grails Karma (#ggx 2014)

Geb

Browser automation solution

jQuery-like selectors

plays well with Grails and Spock

great tool for functional tests

Page 5: Feed Your Grails Karma (#ggx 2014)

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

Page 6: Feed Your Grails Karma (#ggx 2014)

Model Catalogue Plugin

Grails PLugin for managing any

metadata

REST API Backend

AngularJS services and directives

https://github.com/MetadataRegistry/

ModelCataloguePlugin

Page 7: Feed Your Grails Karma (#ggx 2014)

“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

Page 8: Feed Your Grails Karma (#ggx 2014)

Ancient Architecture

ServerFat

Client

Shared Libararies

Page 9: Feed Your Grails Karma (#ggx 2014)

SPA Architecture

REST

backend

Rich

Client

REST API (JSON)

SPA

Page 10: Feed Your Grails Karma (#ggx 2014)

Sample Application(s)

Page 11: Feed Your Grails Karma (#ggx 2014)
Page 12: Feed Your Grails Karma (#ggx 2014)

Earl’s List

Backend application in Grails

Frontend application in AngularJS

with little help of Grails (Asset Pipeline)

https://github.com/musketyr/earls-list

Page 13: Feed Your Grails Karma (#ggx 2014)

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

Page 14: Feed Your Grails Karma (#ggx 2014)

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

Page 15: Feed Your Grails Karma (#ggx 2014)

@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]

}

Page 16: Feed Your Grails Karma (#ggx 2014)

class Item {String descriptionBoolean crossed = Boolean.FALSE

static constraints = {description size: 1..255

}}

Page 17: Feed Your Grails Karma (#ggx 2014)

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

Page 18: Feed Your Grails Karma (#ggx 2014)

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'

}

Page 19: Feed Your Grails Karma (#ggx 2014)

class ItemController extends RestfulController<Item> {

def responseFormats = ['json']

ItemController() {super(Item)

}

}

Page 20: Feed Your Grails Karma (#ggx 2014)

@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" }

}

Page 21: Feed Your Grails Karma (#ggx 2014)

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}

}

Page 22: Feed Your Grails Karma (#ggx 2014)
Page 23: Feed Your Grails Karma (#ggx 2014)

Frontend Application

AngularJS for controllers, services and templates

dependency injection => easier testing

Bootstrap for theming

Jasmine and Karma for testing

Page 24: Feed Your Grails Karma (#ggx 2014)

SPA Architecture

REST

backend

Rich

Client

REST API (JSON)

Page 25: Feed Your Grails Karma (#ggx 2014)

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"

}

Page 26: Feed Your Grails Karma (#ggx 2014)

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+/

}

Page 27: Feed Your Grails Karma (#ggx 2014)

(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);

Page 28: Feed Your Grails Karma (#ggx 2014)

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

Page 29: Feed Your Grails Karma (#ggx 2014)

Where to find frontend tools and libraries?

Page 30: Feed Your Grails Karma (#ggx 2014)
Page 31: Feed Your Grails Karma (#ggx 2014)
Page 32: Feed Your Grails Karma (#ggx 2014)
Page 33: Feed Your Grails Karma (#ggx 2014)
Page 34: Feed Your Grails Karma (#ggx 2014)
Page 35: Feed Your Grails Karma (#ggx 2014)

This slide was intentionally left blank …

Page 36: Feed Your Grails Karma (#ggx 2014)

Front-end Developer Toolbox

Page 37: Feed Your Grails Karma (#ggx 2014)

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

Page 38: Feed Your Grails Karma (#ggx 2014)

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"

}}

Page 39: Feed Your Grails Karma (#ggx 2014)

Bower

Package manager for frontend application

Based on Git

bower.json with similar syntax as package.json (NPM)

bower install

Page 40: Feed Your Grails Karma (#ggx 2014)

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"

}}

Page 41: Feed Your Grails Karma (#ggx 2014)

.bowerrc

{"directory": "grails-app/assets/bower_components"

}

Page 42: Feed Your Grails Karma (#ggx 2014)

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'

]);

Page 43: Feed Your Grails Karma (#ggx 2014)

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>

Page 44: Feed Your Grails Karma (#ggx 2014)

Jasmine

Behaviour-Driven JavaScript

Assertions with matchers

Mocking and Spying

Page 45: Feed Your Grails Karma (#ggx 2014)

describe('Items service wraps the backend', function(){// setupbeforeEach(module('earlslist.items'));

it('should list all items', inject(function(…){// feature method

}));

...}

Page 46: Feed Your Grails Karma (#ggx 2014)

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);

}));

Page 47: Feed Your Grails Karma (#ggx 2014)

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);

}));

Page 48: Feed Your Grails Karma (#ggx 2014)

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);

},

...

};

});

Page 49: Feed Your Grails Karma (#ggx 2014)

Karma

Test runner for JavaScript tests

Can run multiple browsers including HtmlUnit and PhantomJS

karma start karma.conf.json

Page 50: Feed Your Grails Karma (#ggx 2014)

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'

],...

});};

Page 51: Feed Your Grails Karma (#ggx 2014)

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

Page 52: Feed Your Grails Karma (#ggx 2014)

<body ng-app="earlslist"><div class="container"><div ng-include="'earlslist/items.html'"></div>

</div></body>

Page 53: Feed Your Grails Karma (#ggx 2014)

ctrModule.run([‘$templateCache', function($templateCache){

$templateCache.put('earlslist/items.html',

'<div class="row" ng-controller="earlslist.itemsCtrl">' +

...

'</div>');

}]);

Page 54: Feed Your Grails Karma (#ggx 2014)

<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>

Page 55: Feed Your Grails Karma (#ggx 2014)

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;

});};

});

Page 56: Feed Your Grails Karma (#ggx 2014)

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

});}));

Page 57: Feed Your Grails Karma (#ggx 2014)

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);

});

Page 58: Feed Your Grails Karma (#ggx 2014)

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);

}));

Page 59: Feed Your Grails Karma (#ggx 2014)

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

Page 60: Feed Your Grails Karma (#ggx 2014)
Page 61: Feed Your Grails Karma (#ggx 2014)

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

Page 62: Feed Your Grails Karma (#ggx 2014)

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") }

}}

Page 63: Feed Your Grails Karma (#ggx 2014)

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}

}

Page 64: Feed Your Grails Karma (#ggx 2014)
Page 65: Feed Your Grails Karma (#ggx 2014)

„Will it play on continuous integration server?“

Page 66: Feed Your Grails Karma (#ggx 2014)

Travis CI

free for public GitHub repostories

configuration stored in the repository

NodeJS available for every build

Firefox and Chrome for headless testing

Page 67: Feed Your Grails Karma (#ggx 2014)

.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"

Page 68: Feed Your Grails Karma (#ggx 2014)
Page 69: Feed Your Grails Karma (#ggx 2014)

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

Page 70: Feed Your Grails Karma (#ggx 2014)

Thank you

Vladimír Oraný

@musketyr