├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis-deploy.sh ├── .travis.yml ├── LICENSE ├── README.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── server.js ├── src ├── app │ ├── app-component │ │ ├── app-component.js │ │ └── app-component.tpl.html │ ├── app.config.js │ └── app.module.js ├── asset │ └── image │ │ └── testing.png ├── common │ ├── common.module.js │ └── component │ │ └── user-info-component.js ├── feature-a │ ├── feature-a.config.js │ ├── feature-a.module.js │ └── some-component │ │ ├── some-component.js │ │ ├── some-component.test.js │ │ └── some-component.tpl.html ├── feature-b │ ├── feature-b.config.js │ ├── feature-b.constants.js │ ├── feature-b.module.js │ ├── services │ │ ├── todo.service.js │ │ ├── todo.service.spec.js │ │ └── todo.service.test.js │ └── todo-component │ │ ├── todo-component.integration.test.js │ │ ├── todo-component.js │ │ ├── todo-component.test.js │ │ └── todo-component.tpl.html ├── index.html ├── main.css └── main.js ├── webpack.config.js └── webpack.karma.context.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-runtime"], 3 | "presets": ["es2015"] 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-defaults", 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "jasmine": true 9 | }, 10 | "globals": { 11 | "angular": false 12 | }, 13 | "ecmaFeatures": { 14 | "modules": true 15 | } 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | bower_components 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /.travis-deploy.sh: -------------------------------------------------------------------------------- 1 | # go to the out directory and create a *new* Git repo 2 | cd dist 3 | git init 4 | 5 | # inside this git repo we'll pretend to be a new user 6 | git config user.name "Travis CI" 7 | git config user.email "tomas.trajan@gmail.com" 8 | 9 | # The first and only commit to this new Git repo contains all the 10 | # files present with the commit message "Deploy to GitHub Pages". 11 | git add . 12 | git commit -m "Deploy to GitHub Pages" 13 | 14 | # Force push from the current repo's master branch to the remote 15 | # repo's gh-pages branch. (All previous history on the gh-pages branch 16 | # will be lost, since we are overwriting it.) We redirect any output to 17 | # /dev/null to hide any sensitive credential data that might otherwise be exposed. 18 | git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | script: 5 | - npm run ci 6 | - bash ./.travis-deploy.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tomas Trajan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular JS 1.5 - ES6 Module Unit testing and Karma Integration testing seed by [@tomastrajan](https://twitter.com/tomastrajan) [](https://travis-ci.org/tomastrajan/angular-js-es6-testing-example) 2 | 3 | Great seed for enterprise projects with heavy focus on unit and integration testing. 4 | 5 | This repository contains two releases: 6 | 7 | 1. `1.4.0` - for Angular JS 1.4 and lower (uses [Component Pattern for Angular JS](https://medium.com/@tomastrajan/component-paradigm-cf32e94ba78b)) 8 | 2. `1.5.0` - for Angular JS 1.5 and above which supports native `.component(name, options)` API 9 | 10 | ## Info 11 | 12 | * original blog post describing [Proper testing of Angular JS 1.X applications with ES6 modules](https://medium.com/@tomastrajan/proper-testing-of-angular-js-applications-with-es6-modules-8cf31113873f) 13 | * [demo project](http://tomastrajan.github.io/angular-js-es6-testing-example/) with examples of mocha unit & karma integration tests 14 | * [presentation](http://slides.com/tomastrajan/angularjs-unit-testing-with-es6-modules) about the concepts used in this repository 15 | * [video](https://www.youtube.com/watch?v=JTkEsu-cEzc) from Angular JS Meetup Zurich 16 | 17 | 18 |  19 | 20 | ## Getting started 21 | 22 | 1. Clone repository `git clone https://github.com/tomastrajan/angular-js-es6-testing-example.git` 23 | 2. Enter project directory `cd angular-js-es6-testing-example` 24 | 3. Install dependencies `npm i` or `npm install` 25 | 26 | ## Scripts 27 | 28 | All scripts are run with `npm run [script]`, for example: `npm run test`. 29 | 30 | * `start` - start development server, try it by opening `http://localhost:8081/webpack-dev-server/index.html` 31 | 32 | * `build` - create dev build, check `build` directory 33 | * `dist` - create production build, check `dist` directory 34 | 35 | * `server_build` - serve content from `build` directory 36 | * `server_dist` - serve content from `dist` directory 37 | 38 | * `lint` - lint code (with ESLint) 39 | * `mocha` - run all unit tests (with Mocha) 40 | * `watch` - run and watch all unit tests (with Mocha) 41 | * `karma` - run all integration tests (with Karma / Jasmine) 42 | * `test` - lint code and run all tests (with Mocha and Karma) 43 | 44 | * `ci` - for Travis CI 45 | 46 | # Tests 47 | 48 | For more detailed info about tests check the original [blog post](https://medium.com/@tomastrajan/proper-testing-of-angular-js-applications-with-es6-modules-8cf31113873f). 49 | 50 | 51 | * `*.test.js` - mocha unit tests 52 | * `*.integration.test.js` - mocha integration tests (manual) 53 | * `*.spec.js` - karma integration tests (spin up Angular JS app context) 54 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | browsers: ['Chrome'], 6 | frameworks: ['jasmine'], 7 | reporters: ['mocha'], 8 | 9 | logLevel: config.LOG_INFO, 10 | autoWatch: true, 11 | singleRun: false, 12 | colors: true, 13 | port: 9876, 14 | 15 | basePath: '', 16 | files: ['webpack.karma.context.js'], 17 | preprocessors: { 'webpack.karma.context.js': ['webpack'] }, 18 | exclude: [], 19 | webpack: { 20 | devtool: 'eval', 21 | module: { 22 | loaders: [ 23 | {test: /\.js$/, loader: 'babel', exclude: /(\.test.js$|node_modules)/}, 24 | {test: /\.css$/, loader: 'style!css'}, 25 | {test: /\.tpl.html/, loader: 'html'}, 26 | {test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, loader: 'url?limit=50000'} 27 | ] 28 | }, 29 | plugins: [ 30 | new webpack.ProvidePlugin({ 31 | 'window.jQuery': 'jquery' 32 | }) 33 | ] 34 | }, 35 | webpackMiddleware: { 36 | noInfo: true 37 | } 38 | }); 39 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-js-es6-testing-example", 3 | "version": "1.0.0", 4 | "description": "Enhanced testing of Angular JS 1.X applications using ES6 modules", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=DEV webpack-dev-server", 8 | "build": "webpack --display-chunks --display-reasons --display-error-details --display-modules", 9 | "dist": "cross-env NODE_ENV=DIST webpack", 10 | "server_build": "node server.js", 11 | "server_dist": "cross-env NODE_ENV=DIST node server.js", 12 | "lint": "eslint src/", 13 | "mocha": "mocha --compilers js:babel-register ./src/**/*.test.js", 14 | "watch": "mocha --compilers js:babel-register -w ./src/**/*.test.js", 15 | "karma": "karma start", 16 | "test": "npm run lint && npm run mocha && karma start --single-run", 17 | "ci": "npm run lint && npm run mocha && npm run dist" 18 | }, 19 | "author": "tomas.trajan@gmail.com", 20 | "license": "ISC", 21 | "dependencies": { 22 | "angular": "1.5", 23 | "angular-animate": "1.5", 24 | "angular-ui-bootstrap": "0.13.4", 25 | "angular-ui-router": "0.2.15", 26 | "bootstrap": "3.3.5", 27 | "jquery": "2.1.4", 28 | "lodash": "3.10.1" 29 | }, 30 | "devDependencies": { 31 | "angular-mocks": "1.5", 32 | "awesome-typescript-loader": "0.11.3", 33 | "babel-core": "^6.26.3", 34 | "babel-loader": "6.2.1", 35 | "babel-plugin-transform-runtime": "6.4.3", 36 | "babel-preset-es2015": "6.3.13", 37 | "babel-register": "^6.26.0", 38 | "babel-runtime": "6.5", 39 | "chai": "3.2.0", 40 | "chalk": "1.1.0", 41 | "clean-webpack-plugin": "0.1.3", 42 | "connect": "^3.6.6", 43 | "cross-env": "^3.1.3", 44 | "css-loader": "0.15.6", 45 | "eslint": "1.10.3", 46 | "eslint-config-defaults": "8.0.2", 47 | "file-loader": "0.8.4", 48 | "html-loader": "^0.5.5", 49 | "html-webpack-plugin": "2.24.1", 50 | "jasmine-core": "2.3.4", 51 | "karma": "0.13.19", 52 | "karma-chrome-launcher": "0.2.0", 53 | "karma-jasmine": "0.3.6", 54 | "karma-mocha-reporter": "1.1.1", 55 | "karma-webpack": "1.7.0", 56 | "minimist": "1.1.3", 57 | "mocha": "2.2.5", 58 | "ngtemplate-loader": "1.3.0", 59 | "open-browser-webpack-plugin": "0.0.5", 60 | "serve-static": "^1.13.2", 61 | "sinon": "1.17.1", 62 | "style-loader": "0.12.3", 63 | "typescript": "1.5.3", 64 | "url-loader": "0.5.6", 65 | "webpack": "1.10.5", 66 | "webpack-dev-server": "1.10.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var minimist = require('minimist'); 2 | var connect = require('connect'); 3 | var serveStatic = require('serve-static'); 4 | 5 | 6 | var PORT = 8080; 7 | var TARGET_PATH_MAPPING = { 8 | BUILD: './build', 9 | DIST: './dist' 10 | }; 11 | 12 | var TARGET = minimist(process.argv.slice(2)).TARGET || 'BUILD'; 13 | 14 | connect() 15 | .use(serveStatic(TARGET_PATH_MAPPING[TARGET])) 16 | .listen(PORT); 17 | 18 | console.log('Created server for: ' + TARGET + ', listening on port ' + PORT); -------------------------------------------------------------------------------- /src/app/app-component/app-component.js: -------------------------------------------------------------------------------- 1 | export default class AppComponent { 2 | 3 | constructor() { 4 | this.name = 'Tomas'; 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/app/app-component/app-component.tpl.html: -------------------------------------------------------------------------------- 1 |
.component
API available since Angular JS 1.5
10 | Feature A is so great that it even contains counter functionality!
13 |Counter: {{$ctrl.counter}}
The implemented counter functionality is tested in isolation without instantiating of Angular JS context
18 |
30 | import { assert } from 'chai';
31 |
32 | import SomeComponent from './some-component';
33 |
34 | let component;
35 |
36 | describe('some-component', function() {
37 |
38 | beforeEach(function() {
39 | component = new SomeComponent();
40 | });
41 |
42 | it('should start with counter value 20', function () {
43 | assert.equal(component.counter, 20);
44 | });
45 |
46 | it('should accept initial counter value as dependency', function () {
47 | component = new SomeComponent(30);
48 | assert.equal(component.counter, 30);
49 | });
50 |
51 | it('should increment counter value after increment is called', function () {
52 | assert.equal(component.counter, 20);
53 | component.increment();
54 | assert.equal(component.counter, 21);
55 | });
56 |
57 | });
58 |
59 | No `todos` yet, try to add some...
24 |The functionality implemnted in TodoService is tested in isolation without instantiating of Angular JS context
46 |
70 |
71 | import { assert } from 'chai';
72 |
73 | import TodoService from './todo.service.js';
74 |
75 | let service;
76 |
77 | describe('TodoService', function() {
78 |
79 | beforeEach(function() {
80 | service = new TodoService();
81 | });
82 |
83 | it('should contain empty todos after initialization', function () {
84 | assert.equal(service.todos.length, 0);
85 | });
86 |
87 | it('should add todo', function () {
88 | service.addTodo('Finish example project');
89 | assert.equal(service.todos.length, 1);
90 | assert.equal(service.todos[0].label, 'Finish example project');
91 | assert.equal(service.todos[0].done, false);
92 | });
93 |
94 | it('should toggle todo', function () {
95 | service.addTodo('Finish example project');
96 | assert.equal(service.todos[0].done, false);
97 |
98 | service.toggleTodo('Finish example project');
99 | assert.equal(service.todos[0].done, true);
100 |
101 | service.toggleTodo('Finish example project');
102 | assert.equal(service.todos[0].done, false);
103 | });
104 |
105 | it('should remove done todos', function () {
106 | service.addTodo('Todo1');
107 | service.addTodo('Todo2');
108 | service.addTodo('Todo3');
109 | assert.equal(service.todos.length, 3);
110 |
111 | service.toggleTodo('Todo1');
112 | service.removeDoneTodos();
113 | assert.equal(service.todos.length, 2);
114 | });
115 |
116 | });
117 |
118 |
119 |
123 |
124 | import { assert } from 'chai';
125 | import * as sinon from 'sinon';
126 |
127 | import TodoComponent from './todo-component.js';
128 | import TodoService from '../services/todo.service.js';
129 |
130 | let component;
131 | let mockTodoService;
132 |
133 | describe('TodoComponent with mocked service (unit test)', function() {
134 |
135 | beforeEach(function() {
136 | let initialTodos = [];
137 | let TodoServiceInstance = new TodoService(initialTodos);
138 | mockTodoService = sinon.mock(TodoServiceInstance);
139 | component = new TodoComponent(TodoServiceInstance);
140 | });
141 |
142 | afterEach(function() {
143 | mockTodoService.restore();
144 | });
145 |
146 | it('should add todo', function () {
147 | mockTodoService
148 | .expects('addTodo')
149 | .once()
150 | .withArgs('Finish example project');
151 |
152 | component.label = 'Finish example project';
153 | component.addTodo();
154 |
155 | mockTodoService.verify();
156 | });
157 |
158 | it('should toggle todo', function () {
159 | let mockTodo = {label: 'Add unit tests', done: false};
160 | mockTodoService
161 | .expects('toggleTodo')
162 | .twice()
163 | .withArgs(mockTodo.label);
164 |
165 | component.toggleTodo(mockTodo);
166 | component.toggleTodo(mockTodo);
167 |
168 | mockTodoService.verify();
169 | });
170 |
171 | it('should remove done todos', function () {
172 | mockTodoService
173 | .expects('removeDoneTodos')
174 | .once();
175 |
176 | component.removeDoneTodos();
177 |
178 | mockTodoService.verify();
179 | });
180 |
181 | });
182 |
183 |
184 |
188 |
189 | import { assert } from 'chai';
190 |
191 | import TodoComponent from './todo-component.js';
192 | import TodoService from '../services/todo.service.js';
193 |
194 | let component;
195 |
196 | describe('TodoComponent with real service (Integration test)', function() {
197 |
198 | beforeEach(function() {
199 | let initialTodos = [];
200 | let todoService = new TodoService(initialTodos);
201 | component = new TodoComponent(todoService);
202 | });
203 |
204 | it('should contain reference to service\'s todos', function () {
205 | assert.equal(component.todos.length, 0);
206 | });
207 |
208 | it('should add todo', function () {
209 | component.label = 'Finish example project';
210 | component.addTodo();
211 | assert.equal(component.label, '');
212 | assert.equal(component.todos.length, 1);
213 | assert.equal(component.todos[0].label, 'Finish example project');
214 | assert.equal(component.todos[0].done, false);
215 | });
216 |
217 | it('should toggle todo', function () {
218 | component.label = 'Finish example project';
219 | component.addTodo();
220 | assert.equal(component.todos[0].done, false);
221 |
222 | component.toggleTodo(component.todos[0]);
223 | assert.equal(component.todos[0].done, true);
224 |
225 | component.toggleTodo(component.todos[0]);
226 | assert.equal(component.todos[0].done, false);
227 | });
228 |
229 | it('should remove done todos', function () {
230 | component.label = 'Todo1';
231 | component.addTodo();
232 |
233 | component.label = 'Todo2';
234 | component.addTodo();
235 |
236 | component.label = 'Todo2';
237 | component.addTodo();
238 |
239 | assert.equal(component.todos.length, 3);
240 |
241 | component.toggleTodo(component.todos[0]);
242 | component.removeDoneTodos();
243 | assert.equal(component.todos.length, 2);
244 | });
245 |
246 | });
247 |
248 |
249 |