├── screenshot.png ├── app ├── images │ ├── jl.png │ ├── david.png │ ├── eric.png │ ├── loic.png │ ├── romain.png │ ├── lilians.png │ └── mathilde.png ├── directives │ └── score.html ├── app.coffee ├── style.less └── index.html ├── snapshots └── BasketSeleniumTest_list_developers.png ├── src ├── main │ ├── java │ │ └── codestory │ │ │ ├── Basket.java │ │ │ ├── Developer.java │ │ │ ├── IndexResource.java │ │ │ ├── Server.java │ │ │ ├── BasketResource.java │ │ │ ├── Developers.java │ │ │ ├── Tags.java │ │ │ └── BasketFactory.java │ └── resources │ │ ├── tags.json │ │ └── developers.json └── test │ ├── karma.conf.ci.js │ ├── java │ └── codestory │ │ ├── DevelopersTest.java │ │ ├── TagsTest.java │ │ ├── BasketFactoryTest.java │ │ ├── BasketResourceTest.java │ │ ├── BasketSeleniumTest.java │ │ └── BasketRestTest.java │ ├── karma │ └── basketControllerTest.coffee │ └── karma.conf.js ├── package.json ├── pom.xml └── README.md /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/screenshot.png -------------------------------------------------------------------------------- /app/images/jl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/jl.png -------------------------------------------------------------------------------- /app/images/david.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/david.png -------------------------------------------------------------------------------- /app/images/eric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/eric.png -------------------------------------------------------------------------------- /app/images/loic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/loic.png -------------------------------------------------------------------------------- /app/images/romain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/romain.png -------------------------------------------------------------------------------- /app/images/lilians.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/lilians.png -------------------------------------------------------------------------------- /app/images/mathilde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/app/images/mathilde.png -------------------------------------------------------------------------------- /snapshots/BasketSeleniumTest_list_developers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeStory/codestoryway-devoxx14be/master/snapshots/BasketSeleniumTest_list_developers.png -------------------------------------------------------------------------------- /app/directives/score.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/main/java/codestory/Basket.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | public class Basket { 4 | long front; 5 | long back; 6 | long database; 7 | long test; 8 | long hipster; 9 | long sum; 10 | } 11 | -------------------------------------------------------------------------------- /src/test/karma.conf.ci.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./karma.conf.js'); 2 | 3 | module.exports = function (config) { 4 | baseConfig(config); 5 | return config.set({ 6 | singleRun: true, 7 | autoWatch: false 8 | }); 9 | }; -------------------------------------------------------------------------------- /src/main/java/codestory/Developer.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | public class Developer { 4 | public String prenom; 5 | public String job; 6 | public String ville; 7 | public String photo; 8 | public String description; 9 | public String email; 10 | public String[] tags; 11 | public int price; 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/codestory/DevelopersTest.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.*; 6 | 7 | public class DevelopersTest { 8 | Developers developers = new Developers(); 9 | 10 | @Test 11 | public void load_developers() { 12 | Developer[] all = developers.findAll(); 13 | 14 | assertThat(all).hasSize(7); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/codestory/IndexResource.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import net.codestory.http.annotations.*; 4 | import net.codestory.http.templating.*; 5 | 6 | public class IndexResource { 7 | private final Developers developers; 8 | 9 | public IndexResource(Developers developers) { 10 | this.developers = developers; 11 | } 12 | 13 | @Get("/") 14 | public Model index() { 15 | return Model.of("developers", developers.findAll()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/app.coffee: -------------------------------------------------------------------------------- 1 | angular.module 'devoxx', ['ngAnimate'] 2 | 3 | .controller 'BasketController', class 4 | constructor: (@$http)-> 5 | @basket = {} 6 | @emails = [] 7 | 8 | add: (email) -> 9 | @emails.push email 10 | @$http.get("/basket?emails=#{@emails}").success (data) => 11 | @basket = data 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | .directive 'score', -> 22 | scope: 23 | value: '=' 24 | category: '@' 25 | templateUrl: '/directives/score.html' 26 | -------------------------------------------------------------------------------- /src/main/java/codestory/Server.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import net.codestory.http.*; 4 | import net.codestory.http.routes.*; 5 | 6 | public class Server { 7 | public static void main(String[] args) { 8 | new WebServer(new ServerConfiguration()).start(); 9 | } 10 | 11 | public static class ServerConfiguration implements Configuration { 12 | @Override 13 | public void configure(Routes routes) { 14 | routes 15 | .add(IndexResource.class) 16 | .add(BasketResource.class); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": ["Tdd", "Test", "Tests"], 3 | "back": ["Java", "Jee", "Jersey", "Jsp", "Play!", "REST", "Scala", "Spring", "Spring Security", "Spring MVC", "Node", "NodeJS"], 4 | "database": ["Hibernate", "Mongo"], 5 | "front": ["Android", "Angular", "Backbone", "CoffeeScript", "CSS", "Ergonomie", "Graphisme", "Graphiste", "HTML5", "Illustrator", "Indesign", "Javascript", "Marionette", "Logos", "Photoshop", "UI", "UX", "Web", "Webdesign", "Websites"], 6 | "hipster": ["Déploiement Continue", "Git", "Node", "NodeJS", "Mongo", "CoffeeScript", "Angular"] 7 | } -------------------------------------------------------------------------------- /src/test/java/codestory/TagsTest.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.util.*; 6 | 7 | import org.junit.*; 8 | 9 | public class TagsTest { 10 | Tags tags = new Tags(); 11 | 12 | @Test 13 | public void load_tags() { 14 | Map> allTags = tags.findAll(); 15 | 16 | assertThat(allTags).hasSize(5); 17 | } 18 | 19 | @Test 20 | public void count_tags_for_category() { 21 | long count = tags.count("test", "TAG1", "TAG2", "Tdd"); 22 | 23 | assertThat(count).isEqualTo(1); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/codestory/BasketResource.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import java.util.*; 4 | 5 | import net.codestory.http.annotations.*; 6 | 7 | import com.google.common.base.*; 8 | 9 | public class BasketResource { 10 | private final BasketFactory basketFactory; 11 | 12 | public BasketResource(BasketFactory basketFactory) { 13 | this.basketFactory = basketFactory; 14 | } 15 | 16 | @Get("/basket?emails=:emails") 17 | public Basket basket(String emailList) { 18 | List emails = Splitter.on(",").omitEmptyStrings().splitToList(emailList); 19 | 20 | return basketFactory.basket(emails); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/codestory/BasketFactoryTest.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import static java.util.Collections.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | 6 | import org.junit.*; 7 | import org.junit.runner.*; 8 | import org.mockito.*; 9 | import org.mockito.runners.*; 10 | 11 | @RunWith(MockitoJUnitRunner.class) 12 | public class BasketFactoryTest { 13 | @Mock 14 | Developers developers; 15 | @Mock 16 | Tags tags; 17 | 18 | @InjectMocks 19 | BasketFactory basketFactory; 20 | 21 | @Test 22 | public void empty_basket() { 23 | Basket basket = basketFactory.basket(emptyList()); 24 | 25 | assertThat(basket.sum).isZero(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/codestory/Developers.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import java.io.*; 4 | import java.util.stream.*; 5 | 6 | import com.fasterxml.jackson.databind.*; 7 | import com.google.common.io.*; 8 | 9 | public class Developers { 10 | public Developer find(String email) { 11 | return Stream.of(findAll()).filter(dev -> email.equals(dev.email)).findFirst().orElse(null); 12 | } 13 | 14 | Developer[] findAll() { 15 | try { 16 | return new ObjectMapper().readValue(Resources.getResource("developers.json"), Developer[].class); 17 | } catch (IOException e) { 18 | throw new RuntimeException("Unable to load developers list", e); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/codestory/Tags.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | import java.util.stream.*; 6 | 7 | import com.fasterxml.jackson.core.type.*; 8 | import com.fasterxml.jackson.databind.*; 9 | import com.google.common.io.*; 10 | 11 | public class Tags { 12 | public long count(String category, String... developerTags) { 13 | List tagsForCategory = findAll().get(category); 14 | 15 | return Stream.of(developerTags).filter(tag -> tagsForCategory.contains(tag)).count(); 16 | } 17 | 18 | Map> findAll() { 19 | try { 20 | return new ObjectMapper().readValue(Resources.getResource("tags.json"), new TypeReference>>() { 21 | }); 22 | } catch (IOException e) { 23 | throw new RuntimeException("Unable to load tags list", e); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/karma/basketControllerTest.coffee: -------------------------------------------------------------------------------- 1 | should = chai.should() 2 | 3 | describe 'Basket tests', -> 4 | beforeEach -> 5 | module 'devoxx' 6 | inject ($controller, $httpBackend) -> 7 | @controller = $controller 'BasketController' 8 | @http = $httpBackend 9 | 10 | it 'should start with an empty basket', -> 11 | @controller.emails.should.eql [] 12 | @controller.basket.should.eql {} 13 | 14 | it 'should refresh basket after adding a developer', -> 15 | @http.expectGET('/basket?emails=foo@bar.com').respond '{"test":0,"back":0,"database":0,"front":0,"hipster":0,"sum":0}' 16 | @controller.add 'foo@bar.com' 17 | @http.flush() 18 | 19 | @controller.emails.should.eql ['foo@bar.com'] 20 | @controller.basket.should.eql 21 | test: 0 22 | back: 0 23 | database: 0 24 | front: 0 25 | hipster: 0 26 | sum: 0 -------------------------------------------------------------------------------- /src/test/java/codestory/BasketResourceTest.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import static java.util.Arrays.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | import static org.mockito.Mockito.*; 6 | 7 | import org.junit.*; 8 | import org.junit.runner.*; 9 | import org.mockito.*; 10 | import org.mockito.runners.*; 11 | 12 | @RunWith(MockitoJUnitRunner.class) 13 | public class BasketResourceTest { 14 | @Mock 15 | BasketFactory basketFactory; 16 | 17 | @InjectMocks 18 | BasketResource resource; 19 | 20 | @Test 21 | public void create_basket_for_emails() { 22 | Basket expectedBasket = new Basket(); 23 | when(basketFactory.basket(asList("david@devoxx.io", "jeanlaurent@devoxx.io"))).thenReturn(expectedBasket); 24 | 25 | Basket basket = resource.basket("david@devoxx.io,jeanlaurent@devoxx.io"); 26 | 27 | assertThat(basket).isSameAs(expectedBasket); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devoxx-codestory-lab", 3 | "version": "1.0.0", 4 | "description": "Node dependencies for devoxx-codestory-lab", 5 | "main": "index.js", 6 | "author": "Jean-Laurent de Morlhon && David Gageot", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "chai": "^1.9.1", 10 | "coffee-script": "^1.7.1", 11 | "karma": "^0.12.16", 12 | "karma-coffee-preprocessor": "^0.2.1", 13 | "karma-jasmine": "^0.2.2", 14 | "karma-jsmockito-jshamcrest": "0.0.6", 15 | "karma-phantomjs-launcher": "^0.1.4", 16 | "jsmockito": "^1.0.5", 17 | "protractor": "^0.20.1" 18 | }, 19 | "scripts": { 20 | "test": "node_modules/mocha/bin/karma start karma.conf.coffee" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/CodeStory/devoxx-quickstart.git" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/codestory/BasketFactory.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import java.util.*; 4 | import java.util.stream.*; 5 | 6 | public class BasketFactory { 7 | private final Developers developers; 8 | private final Tags tags; 9 | 10 | public BasketFactory(Developers developers, Tags tags) { 11 | this.developers = developers; 12 | this.tags = tags; 13 | } 14 | 15 | public Basket basket(List emails) { 16 | Basket basket = new Basket(); 17 | 18 | Stream developers = emails.stream().map(email -> this.developers.find(email)); 19 | 20 | developers.forEach(developer -> { 21 | basket.test += tags.count("test", developer.tags); 22 | basket.back += tags.count("back", developer.tags); 23 | basket.database += tags.count("database", developer.tags); 24 | basket.front += tags.count("front", developer.tags); 25 | basket.hipster += tags.count("hipster", developer.tags); 26 | basket.sum += developer.price; 27 | }); 28 | 29 | return basket; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | return config.set({ 3 | frameworks: ['jasmine'], 4 | basePath: '../..', 5 | files: [ 6 | 'node_modules/chai/chai.js', 7 | 8 | 'target/webjars/jquery/jquery.min.js', 9 | 'target/webjars/angularjs/angular.min.js', 10 | 'target/webjars/angularjs/angular-animate.min.js', 11 | 'target/webjars/angularjs/angular-mocks.js', 12 | 13 | 'app/app.coffee', 14 | 15 | 'src/test/karma/**/*.coffee' 16 | ], 17 | reporters: ['dots'], 18 | port: 9876, 19 | urlRoot: '/karma/', 20 | browsers: ['PhantomJS'], 21 | captureTimeout: 60000, 22 | logLevel: config.LOG_INFO, 23 | preprocessors: { 24 | '**/*.coffee': ['coffee'] 25 | }, 26 | coffeePreprocessor: { 27 | options: { 28 | sourceMap: true 29 | } 30 | } 31 | }); 32 | }; -------------------------------------------------------------------------------- /src/test/java/codestory/BasketSeleniumTest.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import static codestory.Server.*; 4 | 5 | import net.codestory.http.*; 6 | import net.codestory.simplelenium.*; 7 | 8 | import org.junit.*; 9 | 10 | public class BasketSeleniumTest extends SeleniumTest { 11 | WebServer webServer = new WebServer(new ServerConfiguration()).startOnRandomPort(); 12 | 13 | @Override 14 | public String getDefaultBaseUrl() { 15 | return "http://localhost:" + webServer.port(); 16 | } 17 | 18 | @Test 19 | public void list_developers() { 20 | goTo("/"); 21 | 22 | find(".developer").should().haveSize(7); 23 | find(".developer").should().contain("David", "Jean-Laurent"); 24 | } 25 | 26 | @Test 27 | public void add_one_developer() { 28 | goTo("/"); 29 | 30 | find("#David .add").click(); 31 | 32 | find("#basket .test:not(.ng-hide)").should().haveSize(1); 33 | find("#basket .back:not(.ng-hide)").should().haveSize(1); 34 | find("#basket .database:not(.ng-hide)").should().beEmpty(); 35 | find("#basket .front:not(.ng-hide)").should().haveSize(2); 36 | find("#basket .hipster:not(.ng-hide)").should().haveSize(1); 37 | } 38 | 39 | @Test 40 | public void add_two_developers() { 41 | goTo("/"); 42 | 43 | find("#David .add").click(); 44 | find("#Mathilde .add").click(); 45 | 46 | find("#basket .price").should().contain("1700"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/codestory/BasketRestTest.java: -------------------------------------------------------------------------------- 1 | package codestory; 2 | 3 | import static codestory.Server.*; 4 | import static com.jayway.restassured.RestAssured.*; 5 | import static java.util.Arrays.*; 6 | import static org.hamcrest.CoreMatchers.*; 7 | import static org.mockito.Mockito.*; 8 | import static org.mockito.Mockito.when; 9 | 10 | import net.codestory.http.*; 11 | import net.codestory.http.injection.*; 12 | 13 | import org.junit.*; 14 | 15 | public class BasketRestTest { 16 | WebServer webServer = new WebServer(new ServerConfiguration()).startOnRandomPort(); 17 | 18 | @Test 19 | public void query_basket() { 20 | given().port(webServer.port()) 21 | .when().get("/basket?emails=david@devoxx.io,jeanlaurent@devoxx.io") 22 | .then().contentType("application/json").statusCode(200) 23 | .and().body("sum", equalTo(2000)); 24 | } 25 | 26 | @Test 27 | public void query_scores() { 28 | BasketFactory basketFactory = mock(BasketFactory.class); 29 | Basket basket = basket(4, 3, 0, 3, 5, 2000); 30 | when(basketFactory.basket(asList("david@devoxx.io", "jeanlaurent@devoxx.io"))).thenReturn(basket); 31 | 32 | // Mock the BasketFactory 33 | webServer.configure(routes -> { 34 | new ServerConfiguration().configure(routes); 35 | 36 | Singletons singletons = new Singletons(); 37 | singletons.register(BasketFactory.class, basketFactory); 38 | routes.setIocAdapter(singletons); 39 | }); 40 | 41 | given().port(webServer.port()) 42 | .when().get("/basket?emails=david@devoxx.io,jeanlaurent@devoxx.io") 43 | .then().body("test", equalTo(4)). 44 | and().body("back", equalTo(3)). 45 | and().body("database", equalTo(0)). 46 | and().body("front", equalTo(3)). 47 | and().body("hipster", equalTo(5)). 48 | and().body("sum", equalTo(2000)); 49 | } 50 | 51 | private static Basket basket(int test, int back, int database, int front, int hipster, int sum) { 52 | Basket basket = new Basket(); 53 | basket.test = test; 54 | basket.back = back; 55 | basket.database = database; 56 | basket.front = front; 57 | basket.hipster = hipster; 58 | basket.sum = sum; 59 | return basket; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/style.less: -------------------------------------------------------------------------------- 1 | @header_height: 160px; 2 | 3 | [ng-cloak], .ng-cloack { 4 | display: none !important; 5 | } 6 | 7 | body { 8 | padding-top: @header_height; 9 | } 10 | 11 | .navbar { 12 | height: @header_height; 13 | 14 | .navbar-brand { 15 | font-size: 2em; 16 | } 17 | } 18 | 19 | .profil { 20 | position: relative; 21 | padding: 0 230px 0 20px; 22 | height: 202px; 23 | margin: 10px; 24 | background-color: lighten(lightgrey, 10%); 25 | border: 1px solid lightgrey; 26 | border-radius: 20px; 27 | overflow: hidden; 28 | 29 | h3 { 30 | position: absolute; 31 | top: 0; 32 | right: 230px; 33 | } 34 | 35 | p { 36 | text-align: justify; 37 | } 38 | 39 | img { 40 | position: absolute; 41 | top: 0; 42 | right: 0; 43 | width: 200px; 44 | height: 200px; 45 | } 46 | 47 | .tags { 48 | position: absolute; 49 | bottom: 20px; 50 | } 51 | 52 | .buttons { 53 | position: absolute; 54 | bottom: 20px; 55 | right: 50px; 56 | 57 | .btn { 58 | width: 100px; 59 | } 60 | } 61 | } 62 | 63 | #basket { 64 | margin: 10px 40px 0 0; 65 | width: 220px; 66 | 67 | li { 68 | color: #EDEDED; 69 | list-style: none; 70 | position: relative; 71 | 72 | hr { 73 | margin: 6px 0; 74 | } 75 | 76 | .box { 77 | position: absolute; 78 | width: 16px; 79 | height: 16px; 80 | top: 3px; 81 | } 82 | 83 | .box-1 { 84 | left: calc(60px + (20px * 1)); 85 | } 86 | 87 | .box-2 { 88 | left: calc(60px + (20px * 2)); 89 | } 90 | 91 | .box-3 { 92 | left: calc(60px + (20px * 3)); 93 | } 94 | 95 | .box-4 { 96 | left: calc(60px + (20px * 4)); 97 | } 98 | 99 | .box-5 { 100 | left: calc(60px + (20px * 5)); 101 | } 102 | 103 | .test { 104 | background-color: cornflowerblue; 105 | } 106 | 107 | .back { 108 | background-color: plum; 109 | } 110 | 111 | .database { 112 | background-color: lightsalmon; 113 | } 114 | 115 | .front { 116 | background-color: gold; 117 | } 118 | 119 | .hipster { 120 | background-color: yellowgreen; 121 | } 122 | 123 | .box.ng-hide-add, .box.ng-hide-remove { 124 | transition: all linear 0.5s; 125 | display: block !important; 126 | } 127 | 128 | .box.ng-hide { 129 | opacity: 0; 130 | } 131 | } 132 | } 133 | 134 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Recruteur.io 4 | styles: [/webjars/bootstrap/3.3.0/css/bootstrap.css, style.less] 5 | ng-app: devoxx 6 | --- 7 | 8 |
9 | 46 | 47 |
48 | [[#each developers]] 49 |
50 |

[[prenom]]

51 | 52 |

Tarif [[price]]€ / jour

53 | 54 |

[[job]] - [[ville]]

55 | 56 |

[[description]]

57 | 58 |
59 | [[#each tags]] 60 | [[.]] 61 | [[/each]] 62 |
63 | 64 | 65 | 66 |
67 | Chasser ! 69 | Virer ! 71 |
72 |
73 | [[/each]] 74 |
75 |
76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/main/resources/developers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "email": "david@devoxx.io", 4 | "prenom": "David", 5 | "job": "Développeur Java/Web", 6 | "ville": "Paris, France", 7 | "description": "Bonjour, je suis développeur indépendant. Ma passion ? L'écriture de logiciels pointus mais simples.", 8 | "tags": [ 9 | "Java", "Web", "Javascript", "Agile", "Formation", "Git", "Coaching", "Test" 10 | ], 11 | "photo": "david", 12 | "price": 1000 13 | }, 14 | { 15 | "email": "mathilde@devoxx.io", 16 | "prenom": "Mathilde", 17 | "job": "Ingénieur Mobile / Java", 18 | "ville": "Paris, France", 19 | "description": "Freelance depuis plus de 3 ans, j'interviens sur des projets en tant que développeur / lead technique / scrum master.", 20 | "tags": [ 21 | "Java", "Jee", "Spring", "Hibernate", "Jsp" 22 | ], 23 | "photo": "mathilde", 24 | "price": 700 25 | }, 26 | { 27 | "email": "jeanlaurent@devoxx.io", 28 | "prenom": "Jean-Laurent", 29 | "job": "Blogger du dimanche", 30 | "ville": "Houilles, France", 31 | "description": "Freelance depuis plus de 1 an.", 32 | "tags": [ 33 | "Java", "Test", "CoffeeScript", "Node", "Javascript" 34 | ], 35 | "photo": "jl", 36 | "price": 1000 37 | }, 38 | { 39 | "email": "eric@devoxx.io", 40 | "prenom": "Eric", 41 | "job": "Développeur Java Agile", 42 | "ville": "Houilles, France", 43 | "description": "Je suis professionnel du développement depuis 1998, principalement dans le monde Java. Avec une forte connotation agile.", 44 | "tags": [ 45 | "Java", "Scrum", "Tdd", "Xp", "Mongo" 46 | ], 47 | "photo": "eric", 48 | "price": 700 49 | }, 50 | { 51 | "email": "romain@devoxx.io", 52 | "prenom": "Romain", 53 | "job": "Développeur Java / Web / Agile", 54 | "ville": "Paris, France", 55 | "description": "J'ai une grande passion, l'informatique. Mais j'ai plusieurs amours, oui, c'est possible ! Je vais vous raconter (un peu) ma vie professionnelle....", 56 | "tags": [ 57 | "Java", "Web", "Agile", "Javascript", "Scala", "NodeJS", "REST", "HTML5", "Android", "Play!", "TDD", "Backbone", "Marionette", "Angular", "Git", "Intellij IDEA", "Jersey", "Spring", "Spring MVC", "Spring Security" 58 | ], 59 | "photo": "romain", 60 | "price": 600 61 | }, 62 | { 63 | "email": "lilians@devoxx.io", 64 | "prenom": "Lilians", 65 | "job": "Développeur Java", 66 | "ville": "Viroflay, France", 67 | "description": "Développeur Java expérimenté, j'améliore le confort des autres en mettant en place des outils qui leur permettent de gagner du temps.", 68 | "tags": [ 69 | "Java", "JEE", "Spring", "Tests", "Qualité", "Simplicité", "Maven", "Industrialisation", "Formation", "Déploiement Continue", "Intégration Continue" 70 | ], 71 | "photo": "lilians", 72 | "price": 670 73 | }, 74 | { 75 | "email": "loïc@devoxx.io", 76 | "prenom": "Loïc", 77 | "job": "Graphiste - Art Direction & Webdesign", 78 | "ville": "Paris, France", 79 | "description": "Graphiste free-lance professionnel basé à Paris et référencé au registre des travailleurs indépendants.", 80 | "tags": [ 81 | "UI", "UX", "Webdesign", "Ergonomie", "Graphisme", "Logos", "Websites", "CSS", "Emails", "Illustrator", "Photoshop", "Indesign ", "Graphiste" 82 | ], 83 | "photo": "loic", 84 | "price": 430 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | net.code-story 6 | recruteurio 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 3 11 | 12 | 13 | 14 | UTF-8 15 | 1.8 16 | 1.8 17 | 18 | 19 | 20 | web 21 | 22 | 23 | 24 | maven-jar-plugin 25 | 2.5 26 | 27 | 28 | 29 | true 30 | dependency 31 | net.codestory.Server 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | net.code-story 42 | http 43 | 2.20 44 | 45 | 46 | com.google.guava 47 | guava 48 | 18.0 49 | 50 | 51 | 52 | 53 | org.webjars 54 | bootstrap 55 | 3.3.0 56 | 57 | 58 | org.webjars 59 | angularjs 60 | 1.3.0 61 | 62 | 63 | 64 | 65 | junit 66 | junit 67 | 4.12-beta-2 68 | test 69 | 70 | 71 | org.mockito 72 | mockito-all 73 | 1.10.8 74 | test 75 | 76 | 77 | org.assertj 78 | assertj-core 79 | 1.7.0 80 | test 81 | 82 | 83 | com.jayway.restassured 84 | rest-assured 85 | 2.3.4 86 | test 87 | 88 | 89 | net.code-story 90 | simplelenium 91 | 1.25 92 | test 93 | 94 | 95 | 96 | 97 | 98 | karma 99 | 100 | 101 | !skipTests 102 | 103 | 104 | 105 | 106 | 107 | org.codehaus.mojo 108 | exec-maven-plugin 109 | 1.3.2 110 | 111 | 112 | extract webjars 113 | 114 | java 115 | 116 | generate-test-resources 117 | 118 | 119 | 120 | net.codestory.http.misc.ExtractWebjars 121 | 122 | 123 | 124 | com.github.eirslett 125 | frontend-maven-plugin 126 | 0.0.16 127 | 128 | ${project.basedir} 129 | 130 | 131 | 132 | install node and npm 133 | 134 | install-node-and-npm 135 | 136 | generate-test-resources 137 | 138 | v0.10.29 139 | 1.4.16 140 | 141 | 142 | 143 | npm install 144 | 145 | npm 146 | 147 | generate-test-resources 148 | 149 | install 150 | 151 | 152 | 153 | karma tests 154 | 155 | karma 156 | 157 | test 158 | 159 | ${project.basedir}/src/test/karma.conf.ci.js 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern java webapp 2 | 3 | ## The CodeStory Way 4 | by David Gageot & Jean-Laurent de Morlhon 5 | 6 | ## Abstract 7 | 8 | Come participate to this 3 hours Hand's On lab. Our target: teach you to program a modern webapp in Java (yes Java!), quickly, pragmatically and with ease. 9 | 10 | With the help and live code demos of David Gageot & Jean-Laurent de Morlhon. 11 | 12 | The menu: Java 8, some AngularJs, a taste of CoffeeScript, Pair Programming, UI tests, hotkeys you didn't knew existed, plugins from outer space and an ultra fast development cycle. Yes, we're still talking about Java. 13 | 14 | Monday, at work, you won't see your java project the same way. 15 | 16 | # Install party 17 | 18 | To attend this workshop in the best possible conditions: 19 | 20 | * A laptop with enough power for 3 hours 21 | * A teammate 22 | * Software: 23 | * Java 8 24 | * Maven 3.1 25 | * An IDE 26 | * A few graphical assets you'll find in this repo or on a usb key, network drive we will share during the session. 27 | 28 | # Recruteur.io 29 | 30 | You friend Jean-Claude from a famous second-zone business school in the countryside has a tremendous idea: 31 | 32 | We're going to make a website to find programmers, and make a ton of money *selling* them to customers across the world to help write web applications. 33 | 34 | Jean-Claude has heard that we probably need different skill sets to build a great team. You need to mix these skills. 35 | 36 | In 2014, Jean-Claude has decided you need 4 different skills: `Front`, `Back`, `Database`, `Test`. And to sound more appealing and also because it sells well, he added a fifth skill named `Hipster`. 37 | 38 | The website could look like this: 39 | 40 | ![Screenshot](./screenshot.png) 41 | 42 | Martine from HR has already bought the domain name on godady, you're free to go. 43 | We have installed FrontPage and IIS on your laptop, you've got 3 hours! 44 | 45 | # Let's write some code 46 | 47 | ## Server startup 48 | 49 | 1. Create a blank directory, in which you add a pom.xml which looks like: 50 | 51 | ```xml 52 | 54 | 4.0.0 55 | 56 | net.code-story 57 | recruteurio 58 | 1.0-SNAPSHOT 59 | 60 | 61 | 1.8 62 | 1.8 63 | UTF-8 64 | 65 | 66 | 67 | 68 | net.code-story 69 | http 70 | 2.20 71 | 72 | 73 | 74 | ``` 75 | 76 | So, yes, we're going to use Java 8. We have waited for it for too long, not to use it right away. Fasten your seat belts. 77 | 78 | 1. Then you create, like a grownup, the source & test directories (yeah, we got tests too, I know so modern...). 79 | 80 | ```bash 81 | mkdir -p src/{main,test}/java 82 | ``` 83 | 84 | (btw you can create them with your mouse, but it's less hype and stylish. Modern web remember?) 85 | 86 | 1. We are here to make a web-app. But we are going to be classical for a change and start with a good old 'Hello World'. 87 | 88 | You should create an `index.html` at the root of an `app` directory beside your `pom.xml` like this: 89 | 90 | ```bash 91 | mkdir app 92 | touch app/index.html 93 | ``` 94 | 95 | Then edit your `index.html` and type in: 96 | 97 | ```html 98 | --- 99 | layout: default 100 | title: Hello Devoxx 101 | --- 102 | 103 |

Hello Devoxx

104 | 105 |

I can serve a web page in a java app in less than 2 minutes... Yes, I can!

106 | ``` 107 | 108 | Before you ask, the header in between dashes is called Yaml Front Matter. You can enter a bunch of information using Yaml syntax there and everything after the last `---` is going to be plain old HTML. You can do crazy stuff in here, if you're nice you'll see a glimpse of it, but we won't go into more details there. But trust us, it's quite convenient. 109 | 110 | In fluent-http, everything you put in the `app` directory is served at the root of your web-app. 111 | If you put an html file, it will be serve, as-is. Same for js files, images etc... 112 | 113 | If you put some Less files, they will be compiled to css and served (with a cache don't worry), the same applies to Coffeescript compiled to Javascript, Markdown to Html and a few others. 114 | 115 | 1. Ok it's a java workshop or what? When will I write some Java Code?!: 116 | 117 | Just about now : In `src/main/java` create a `Server` class. Like this one: 118 | 119 | ```java 120 | import net.codestory.http.*; 121 | 122 | public class Server { 123 | public static void main(String[] args) { 124 | new WebServer().start(); 125 | } 126 | } 127 | ``` 128 | 129 | 1. Then you execute the `Server` class, open a browser and aim it towards http://localhost:8080 130 | If everything goes according to the plan, just about now, you'll feel less inclined to use weblo or tomcat, monday at work. I started a java program that serves content in 1 line of code and 5 minutes... 131 | 132 | (If you're on the fancy side of stuff, and that you change your working dir, I know *crazy*, but some of you do it, you'll have to point your working dir to the root of your app. It's usually done in the working dir input field in the run class dialog of your IDE) 133 | 134 | ## Server Side Mustaches with Handlebars 135 | 136 | 1. Fluent-http provides some kind of server side, logic less, templating. 137 | 138 | Change your server to the following, we add a route to '/' which defines a conference variable to be used in your template in a Java8-lambdaish way. 139 | 140 | ```java 141 | import net.codestory.http.templating.*; 142 | 143 | public class Server { 144 | public static void main(String[] args) { 145 | new WebServer(routes -> routes 146 | .get("/", () -> Model.of("conference", "Devoxx"))) 147 | .start(); 148 | } 149 | } 150 | ``` 151 | 152 | Change your index.html to : 153 | 154 | ```Html 155 | --- 156 | layout: default 157 | title: hello Devoxx 158 | conference: Devoxx 159 | --- 160 | 161 |

Hello [[conference]]!

162 | ``` 163 | 164 | The templating language used here is [Handlebars](http://handlebarsjs.com/). You can use every handlebar instruction but within `[[` and `]]` instead the usual `{{` and `}}`. As you may know, we are going to use some angularJs in a few minutes, so we changed the way handlebars detects it's tag so that it doesn't clash with angularJs. You can then use a mix of server side and client side content. 165 | 166 | Woot! Some Handlebars and some Java 8 lambda at the same time. Everything is rendered server side. Consider it the jsp of 2014. 167 | 168 | ### Handlebars supports Loops 169 | 170 | In the app, Jean-Clause wants us to display a bunch of developers, so we need a way to iterate through a list of developers: 171 | 172 | ```java 173 | import net.codestory.http.templating.*; 174 | 175 | public class Server { 176 | public static void main(String[] args) { 177 | new WebServer(routes -> routes 178 | .get("/", () -> Model.of("developers", Arrays.asList("David", "Jean-Laurent")))) 179 | .start(); 180 | } 181 | } 182 | ``` 183 | 184 | Display the content like this: 185 | 186 | ```html 187 | [[#each developers]] 188 | [[.]] 189 | [[/each]] 190 | ``` 191 | 192 | ### You can use Java Beans, Pojos, Java Objects, you name it. 193 | 194 | But developers aren't defined only by their names, (we tend to say the define themselves by the number of bugs they produces but that's another story). Developers needs properties let's start simply with name and price: 195 | 196 | ```java 197 | public class Developer { 198 | String name; 199 | int price; 200 | 201 | public Developer() { 202 | } 203 | 204 | public Developer(String name, int price) { 205 | this.name = name; 206 | this.price = price; 207 | } 208 | } 209 | 210 | public class Server { 211 | public static void main(String[] args) { 212 | new WebServer(routes -> routes 213 | .get("/", () -> Model.of("developers", Arrays.asList( 214 | new Developer("David", 1000), 215 | new Developer("Jean-Laurent", 1000))))) 216 | .start(); 217 | } 218 | } 219 | ``` 220 | 221 | Then you can display developer's fields. 222 | 223 | ``` 224 |
    225 | [[#each developers]] 226 |
  • [[name]] [[price]]
  • 227 | [[/each]] 228 |
229 | ``` 230 | 231 | You can do many more things in Handlebars, but keep in mind it's call logic less for a reason, you can see more at: http://handlebarsjs.com/. 232 | 233 | ### Tests 234 | 235 | We are going to extract a `Configuration` object to make it usable for tests. Like this: 236 | 237 | ```java 238 | public class Server { 239 | public static void main(String[] args) { 240 | new WebServer(new ServerConfiguration()).start(); 241 | } 242 | 243 | public static class ServerConfiguration implements Configuration { 244 | @Override 245 | public void configure(Routes routes) { 246 | routes.get("/", () -> Model.of("developers", Arrays.asList( 247 | new Developer("David", 1000), 248 | new Developer("Jean-Laurent", 1000) 249 | ))); 250 | } 251 | } 252 | } 253 | ``` 254 | 255 | Let's write an end to end test, also called sometimes acceptance test, UI test or **test-which-brake-too-often-but-are-really-really-life-saver(tm)**. 256 | 257 | So take an hour, setup selenium, install all drivers. Just kidding! 258 | We do everything for you, with our hand-cooked selenium wrapper called Simplelenium 259 | 260 | ```xml 261 | 262 | net.code-story 263 | simplelenium 264 | 1.25 265 | test 266 | 267 | 268 | junit 269 | junit 270 | 4.11 271 | test 272 | 273 | ``` 274 | 275 | ```java 276 | import net.codestory.http.WebServer; 277 | import net.codestory.simplelenium.SeleniumTest; 278 | import org.junit.Test; 279 | 280 | import static net.codestory.Server.ServerConfiguration; 281 | 282 | public class BasketSeleniumTest extends SeleniumTest { 283 | WebServer webServer = new WebServer(new ServerConfiguration()).startOnRandomPort(); 284 | 285 | @Override 286 | public String getDefaultBaseUrl() { 287 | return "http://localhost:" + webServer.port(); 288 | } 289 | 290 | @Test 291 | public void list_developers() { 292 | goTo("/"); 293 | 294 | find(".developer").should().haveSize(2); 295 | find(".developer").should().contain("David", "Jean-Laurent"); 296 | } 297 | } 298 | ``` 299 | 300 | Don't need to install Chrome, Selenium, PhantomJS or what. It just works. 301 | 302 | To avoid port conflicts (two server asking for the same port) with tests running in parallel, fluent-http gives you a `startOnRandomPort()` method which makes sure conflicts are avoided. 303 | 304 | ## Simple REST Service 305 | 306 | Routes can be written with Java 8 Lambdas. But for more complex routes, it's best to extract the route's code into a `Resource` class. And because fluent-http is built from the ground up to be a web container, every time it sees a Java Bean or Pojo in a resource method signature it exposes it as `json` by default. 307 | 308 | For instance if your route needs to server a `Basket``, it could be defined like this : 309 | 310 | ```java 311 | public class Basket { 312 | long front; 313 | long back; 314 | long database; 315 | long test; 316 | long hipster; 317 | long sum; 318 | } 319 | ``` 320 | 321 | You can easily add a resource to you http server like this: 322 | 323 | ```java 324 | public class BasketResource { 325 | @Get("/basket") 326 | public Basket basket() { 327 | return new Basket(); 328 | } 329 | } 330 | ``` 331 | 332 | Then you add it to your routes: 333 | 334 | ```Java 335 | public class ServerConfiguration implements Configuration { 336 | @Override 337 | public void configure(Routes routes) { 338 | routes.add(BasketResource.class); 339 | } 340 | } 341 | ``` 342 | 343 | And when you call http://localhost:8080/basket you get something like: 344 | 345 | ```json 346 | { 347 | "front":0, 348 | "back":0, 349 | ... 350 | "sum":0, 351 | } 352 | ``` 353 | 354 | Now that we created our first resource, let's extract another resource for the index page. 355 | 356 | ```java 357 | import net.codestory.http.annotations.Get; 358 | import net.codestory.http.templating.Model; 359 | 360 | public class IndexResource { 361 | @Get("/") 362 | public Model index() { 363 | return Model.of("developers", Arrays.asList( 364 | new Developer("David", 1000), 365 | new Developer("Jean-Laurent", 1000) 366 | )); 367 | } 368 | } 369 | ``` 370 | 371 | And plug it this way: 372 | 373 | ```java 374 | public class ServerConfiguration implements Configuration { 375 | @Override 376 | public void configure(Routes routes) { 377 | routes 378 | .add(IndexResource.class) 379 | .add(BasketResource.class); 380 | } 381 | } 382 | ``` 383 | 384 | ## Integration testing with RestAssured 385 | 386 | Integration tests at the resource level are interesting because it's the only way to check that our domain code is properly wrapped into a REST resource. 387 | You should concentrate on testing on http input/output. While mocking/stubbing the domain code. 388 | 389 | We use the RestAssured library which offers a fluent API to write tests. Testing the http interaction layer is quite tedious to write. 390 | 391 | Add to your pom the dependency: 392 | 393 | ```xml 394 | 395 | com.jayway.restassured 396 | rest-assured 397 | 2.3.4 398 | test 399 | 400 | ``` 401 | 402 | RestAssured needs a real http server. This is usually done in the integration testing phase through the failsafe maven plugin. But we are crazy modern guys, we don't want to distinguish those tests since we are able to execute integration test at almost the same speed as unit tests. 403 | 404 | To be able to have integration test execute as fast as unit test, you need a lighting fast webserver, that's why we use fluent-http. It's very good at it. 405 | Less configuration, lighting speed, you saved yourself at many hours writing xml in your project. You're welcome... 406 | 407 | Here's a typical skeleton for a REST test: 408 | 409 | ```java 410 | import static com.jayway.restassured.RestAssured.*; 411 | 412 | public class BasketRestTest { 413 | WebServer webServer = new WebServer(new ServerConfiguration()).startOnRandomPort(); 414 | 415 | @Test 416 | public void query_basket() { 417 | given().port(webServer.port()) 418 | .when().get("/basket") 419 | .then().contentType("application/json").statusCode(200); 420 | } 421 | } 422 | ``` 423 | 424 | ## AngularJs 425 | 426 | To add angularjs the java-way you can use Webjars. Webjars are a collection of javascript libraries packaged in a jar, properly registered on a maven central repository. (Don''t tell the javascript fans about this. They'd have a ceasure) 427 | 428 | ```xml 429 | 430 | org.webjars 431 | angularjs 432 | 1.3.0 433 | 434 | ``` 435 | 436 | You'll use the `/webjars/angularjs/1.3.0/angular.min.js` path in a ` 475 | 476 | ``` 477 | 478 | Notice how you write a `main.coffee` file but reference a `main.js` file in the html? Fluent-http let 479 | you choose if you prefer to write coffee or javascript. If you prefer coffee, then scripts are compiled 480 | server-side transparently on the fly. 481 | 482 | # Unit, integration, javascript & ui Testing! 483 | 484 | ## Resource Unit Testing with JUnit 485 | 486 | Nothing, *that* modern in here. 487 | 488 | We use the usual suspects of the industry here, `AssertJ` (fluent assertions) & `Mockito` (mocking). 489 | 490 | You can add those two libraries in your pom: 491 | 492 | ```xml 493 | 494 | org.assertj 495 | assertj-core 496 | 1.7.0 497 | test 498 | 499 | 500 | 501 | org.mockito 502 | mockito-all 503 | 1.10.8 504 | test 505 | 506 | ``` 507 | 508 | Let's write the resources we need to do our app for Jean-Claude. 509 | 510 | We need some kind of developer domain object: 511 | 512 | ```java 513 | public class Developer { 514 | public String prenom; 515 | public String job; 516 | public String ville; 517 | public String photo; 518 | public String description; 519 | public String email; 520 | public String[] tags; 521 | public int price; 522 | } 523 | ``` 524 | 525 | We need some kind of developers list: 526 | 527 | ```json 528 | [ 529 | { 530 | "email": "david@devoxx.io", 531 | "prenom": "David", 532 | "job": "Java/Web Developer", 533 | "ville": "Paris, France", 534 | "description": "Bonjour, je suis développeur indépendant. Ma passion ? L'écriture de logiciels pointus mais simples.", 535 | "tags": [ 536 | "Java", "Web", "Javascript", "Agile", "Formation", "Git", "Coaching", "Test" 537 | ], 538 | "photo": "david", 539 | "price": 1000 540 | }, 541 | { 542 | "email": "jeanlaurent@devoxx.io", 543 | "prenom": "Jean-Laurent", 544 | "job": "Programmer", 545 | "ville": "Houilles, France", 546 | "description": "WILL WRITE CODE FOR FOOD", 547 | "tags": [ 548 | "Java", "Test", "CoffeeScript", "Node", "Javascript" 549 | ], 550 | "photo": "jl", 551 | "price": 1000 552 | } 553 | ] 554 | ``` 555 | 556 | So let's write our own in-memory version of an "Oracle Database": 557 | 558 | ```java 559 | import com.fasterxml.jackson.databind.ObjectMapper; 560 | import com.google.common.io.Resources; 561 | 562 | public class Developers { 563 | public Developer find(String email) { 564 | return Stream.of(findAll()).filter(dev -> email.equals(dev.email)).findFirst().orElse(null); 565 | } 566 | 567 | Developer[] findAll() { 568 | try { 569 | return new ObjectMapper().readValue(Resources.getResource("developers.json"), Developer[].class); 570 | } catch (IOException e) { 571 | throw new RuntimeException("Unable to load developers list", e); 572 | } 573 | } 574 | } 575 | ``` 576 | 577 | The `Resources` class comes from guava. Be careful, fluent-http also has a `Resources` class but 578 | it not the one we want to use here. If you're a Java developer and you don't know Guava, 579 | you've been coding from inside a cave and you maybe you should take a look at it right now. 580 | 581 | ```java 582 | 583 | com.google.guava 584 | guava 585 | 18.0 586 | 587 | ``` 588 | 589 | Here's a corresponding tests: 590 | Yes it's a test based on data. No it's not perfect. Yes it's a good example of unit testing. 591 | 592 | ```java 593 | import org.junit.Test; 594 | 595 | import static org.assertj.core.api.Assertions.assertThat; 596 | 597 | public class DevelopersTest { 598 | @Test 599 | public void load_developers() { 600 | Developer[] developers = new Developers().findAll(); 601 | 602 | assertThat(developers).hasSize(2); 603 | } 604 | } 605 | ``` 606 | 607 | ## Unit testing angular controller with Karma 608 | 609 | We'd like to unit test our angular controller. E2e tests are too slow to test everything. 610 | Rest tests don't test the javascript. So we are going to put a foot in the javascript world, 611 | using **Karma** and **Jasmine**. 612 | 613 | We'll use `angular-mocks` (a default testing library for angular) in conjunction with `chai.js` which gives us nice fluent assertions. The testing library used here is `jasmine`. The syntax is `coffeescript`. 614 | 615 | Because we live in the java/maven world, out project need a little configuration first. Once it's done 616 | everybody on the team will be happy not to install node, karma, jasmine and al. manually. 617 | 618 | ## Node configuration 619 | 620 | This is a `package.json` file, it's the `pom.xml` in the node world. It enables us to define all the libraries and dependencies we need for karma to launch properly. Create it at the root of your project. 621 | 622 | ```json 623 | { 624 | "name": "devoxx-codestory-lab", 625 | "version": "1.0.0", 626 | "description": "Node dependencies for devoxx-codestory-lab", 627 | "main": "index.js", 628 | "author": "Jean-Laurent de Morlhon && David Gageot", 629 | "license": "MIT", 630 | "devDependencies": { 631 | "chai": "^1.9.1", 632 | "coffee-script": "^1.7.1", 633 | "karma": "^0.12.16", 634 | "karma-coffee-preprocessor": "^0.2.1", 635 | "karma-jasmine": "^0.2.2", 636 | "karma-jsmockito-jshamcrest": "0.0.6", 637 | "karma-phantomjs-launcher": "^0.1.4", 638 | "jsmockito": "^1.0.5", 639 | "protractor": "^0.20.1" 640 | }, 641 | "scripts": { 642 | "test": "node_modules/mocha/bin/karma start karma.conf.coffee" 643 | }, 644 | "repository": { 645 | "type": "git", 646 | "url": "git://github.com/CodeStory/devoxx-quickstart.git" 647 | } 648 | } 649 | ``` 650 | 651 | ## maven front-end plugin 652 | 653 | Close your eyes, welcome to the wonderful world of maven xml and plugins: 654 | The maven front-end plugin is able to do dirty stuff you don't want to have to do by hand, especially if you haven't done node stuff recently. So the price to pay is the following horribles 40 lines. But trust us, it's a life saver, it automates reliably the process of launching javascript tests in the java world. 655 | 656 | ```xml 657 | 658 | 659 | karma 660 | 661 | 662 | !skipTests 663 | 664 | 665 | 666 | 667 | 668 | org.codehaus.mojo 669 | exec-maven-plugin 670 | 1.3.2 671 | 672 | 673 | extract webjars 674 | 675 | java 676 | 677 | generate-test-resources 678 | 679 | 680 | 681 | net.codestory.http.misc.ExtractWebjars 682 | 683 | 684 | 685 | com.github.eirslett 686 | frontend-maven-plugin 687 | 0.0.16 688 | 689 | ${project.basedir} 690 | 691 | 692 | 693 | install node and npm 694 | 695 | install-node-and-npm 696 | 697 | generate-test-resources 698 | 699 | v0.10.29 700 | 1.4.16 701 | 702 | 703 | 704 | npm install 705 | 706 | npm 707 | 708 | generate-test-resources 709 | 710 | install 711 | 712 | 713 | 714 | karma tests 715 | 716 | karma 717 | 718 | test 719 | 720 | ${project.basedir}/src/test/karma.conf.ci.js 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | ``` 730 | 731 | To finish the configuration, add these too files in `src/test`: 732 | 733 | src/test/karma.conf.js: 734 | 735 | ```javascript 736 | module.exports = function (config) { 737 | return config.set({ 738 | frameworks: ['jasmine'], 739 | basePath: '../..', 740 | files: [ 741 | 'node_modules/chai/chai.js', 742 | 743 | 'target/webjars/jquery/jquery.min.js', 744 | 'target/webjars/angularjs/angular.min.js', 745 | 'target/webjars/angularjs/angular-animate.min.js', 746 | 'target/webjars/angularjs/angular-mocks.js', 747 | 748 | 'app/app.coffee', 749 | 750 | 'src/test/karma/**/*.coffee' 751 | ], 752 | reporters: ['dots'], 753 | port: 9876, 754 | urlRoot: '/karma/', 755 | browsers: ['PhantomJS'], 756 | captureTimeout: 60000, 757 | logLevel: config.LOG_INFO, 758 | preprocessors: { 759 | '**/*.coffee': ['coffee'] 760 | }, 761 | coffeePreprocessor: { 762 | options: { 763 | sourceMap: true 764 | } 765 | } 766 | }); 767 | }; 768 | ``` 769 | 770 | and karma.conf.ci.js: 771 | 772 | ```javascript 773 | var baseConfig = require('./karma.conf.js'); 774 | 775 | module.exports = function (config) { 776 | baseConfig(config); 777 | return config.set({ 778 | singleRun: true, 779 | autoWatch: false 780 | }); 781 | };``` 782 | 783 | ## A test at last 784 | 785 | Everything is configured! Le't write our first test in `src/test/karma/basketControllerTest.coffee`: 786 | 787 | ```coffee 788 | should = chai.should() 789 | 790 | describe 'Basket tests', -> 791 | beforeEach -> 792 | module 'devoxx' 793 | inject ($controller) -> 794 | @controller = $controller 'BasketController' 795 | 796 | it 'should start with an empty basket', -> 797 | @controller.emails.should.eql [] 798 | @controller.basket.should.eql {} 799 | ``` 800 | 801 | Because, we code controllers in coffee, we write tests in coffee. 802 | 803 | To run the tests, your can either trigger a full build with `mvn clean install` or run only 804 | the karma tests with `./node_modules/karma/bin/karma start src/test/karma.conf.js` 805 | 806 | ### Testing the http calls 807 | 808 | Here you can write a more complicated test, to handle some tricky situation where your angular controller is making an http call (which occurs... very often). 809 | 810 | ```coffee 811 | should = chai.should() 812 | 813 | describe 'Basket tests', -> 814 | beforeEach -> 815 | module 'devoxx' 816 | inject ($controller, $httpBackend) -> 817 | @controller = $controller 'BasketController' 818 | @http = $httpBackend 819 | 820 | it 'should refresh basket after adding a developer', -> 821 | @http.expectGET('/basket?emails=foo@bar.com').respond '{"test":0,"back":0,"database":0,"front":0,"hipster":0,"sum":0}' 822 | @controller.add 'foo@bar.com' 823 | @http.flush() 824 | 825 | @controller.emails.should.eql ['foo@bar.com'] 826 | @controller.basket.should.eql 827 | test: 0 828 | back: 0 829 | database: 0 830 | front: 0 831 | hipster: 0 832 | sum: 0 833 | ``` 834 | 835 | ## Deuglifying the page. 836 | 837 | You can write your own css, but you can also use Twitter Bootstrap to ease the pain of spending two hours having 3 divs side by side. To add bootstrap you can use Webjars. You add to your pom: 838 | 839 | ```xml 840 | 841 | org.webjars 842 | bootstrap 843 | 3.3.0 844 | 845 | ``` 846 | 847 | If you use the YAML Front Matter you can easily add in the header: 848 | 849 | ```YAML 850 | --- 851 | title: recruteur.io 852 | styles: ['/webjars/bootstrap/3.1.1/css/bootstrap.css'] 853 | --- 854 | ``` 855 | 856 | Now you should have everything you need to finish the app. 857 | 858 | What are you waiting?? Jean-Claude is not a patient man and as during estimation phase you said the project could take between 3 hours to two days, Jean-Claude thinks you can make it in 2 hours, or your job will be outsourced in a far away country, where developers are cheap. 859 | 860 | -- David & Jean-Laurent 861 | --------------------------------------------------------------------------------