├── test ├── mocha.opts ├── test.ts.template ├── exercise1 │ └── word.ts ├── exercise3 │ └── minesweeper.ts ├── example │ ├── MechanicalThings │ │ ├── car.ts │ │ └── plane.ts │ └── driver.ts ├── exercise2 │ └── calculator.ts ├── index.ts └── exercise4 │ ├── engine.ts │ └── terminal.ts ├── assets └── results.png ├── grunt ├── storeCoverage.js ├── instrument.js ├── makeReport.js ├── coverage.js ├── ts.js ├── watch.js ├── clean.js ├── mochaTest.js ├── replace.js └── concat.js ├── blanket.js ├── src ├── example │ ├── index.ts │ ├── MechanicalThings │ │ ├── index.ts │ │ ├── car.ts │ │ └── plane.ts │ ├── driver.ts │ └── README.md ├── exercise1 │ ├── word.ts │ └── README.md ├── exercise4 │ ├── run.ts │ ├── engine.ts │ ├── terminal.ts │ └── README.md ├── exercise2 │ ├── calculator.ts │ └── README.md └── exercise3 │ ├── minesweeper.ts │ └── README.md ├── .travis.yml ├── tsd.json ├── typings └── blessed.d.ts ├── gruntfile.js ├── references.ts ├── package.json ├── .gitignore └── README.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui tdd 2 | --growl -------------------------------------------------------------------------------- /assets/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michikono/typescript-tdd-exercises/HEAD/assets/results.png -------------------------------------------------------------------------------- /grunt/storeCoverage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | dir: 'coverage/reports' 4 | } 5 | }; -------------------------------------------------------------------------------- /blanket.js: -------------------------------------------------------------------------------- 1 | require('blanket')({ 2 | // Only files that match the pattern will be instrumented 3 | pattern: ['out/src'] 4 | }); -------------------------------------------------------------------------------- /grunt/instrument.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: [ 3 | 'out/src/**/*.js' 4 | ], 5 | options: { 6 | lazy: false, 7 | basePath: 'out/instrument/' 8 | } 9 | }; -------------------------------------------------------------------------------- /src/example/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Example { 4 | export interface Coordinate { 5 | x: number 6 | y: number 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /grunt/makeReport.js: -------------------------------------------------------------------------------- 1 | // Empties folders to start fresh 2 | module.exports = { 3 | src: 'coverage/reports/**/*.json', 4 | options: { 5 | type: 'lcov', 6 | dir: 'coverage/reports', 7 | print: 'detail' 8 | } 9 | }; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | - npm install 10 | - sleep 3 # give server time to start 11 | 12 | script: 13 | - npm test 14 | -------------------------------------------------------------------------------- /test/test.ts.template: -------------------------------------------------------------------------------- 1 | /// 2 | var assert = require("assert"); 3 | var sinon: SinonStatic = require("sinon"); 4 | 5 | describe('EXAMPLE MODULE OR CLASS', () => { 6 | describe('METHODTOTEST()', () => { 7 | it('should DO SOME BEHAVIOR WHEN SOME CONDITION', () => { 8 | 9 | }); 10 | }); 11 | }); -------------------------------------------------------------------------------- /grunt/coverage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: { 3 | options: { 4 | thresholds: { 5 | statements: 95, 6 | branches: 95, 7 | lines: 95, 8 | functions: 95 9 | }, 10 | dir: 'coverage/reports', 11 | root: '.' 12 | } 13 | } 14 | }; -------------------------------------------------------------------------------- /grunt/ts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: { 3 | src: ['references.ts', 'src/**/*.ts', 'test/**/*.ts'], 4 | outDir: 'out', 5 | options: { 6 | fast: 'never', 7 | module: 'commonjs', 8 | comments: true, 9 | target: 'es5' 10 | }, 11 | reference: 'references.ts' 12 | } 13 | }; -------------------------------------------------------------------------------- /src/example/MechanicalThings/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Example { 4 | export module MechanicalThings { 5 | export interface Transportation { 6 | sound: () => string 7 | move: (offset:Coordinate) => void 8 | velocity: () => number 9 | position: () => Coordinate 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /grunt/watch.js: -------------------------------------------------------------------------------- 1 | // Watches files for changes and runs tasks based on the changed files 2 | module.exports = { 3 | ts: { 4 | options: { 5 | reload: true 6 | }, 7 | files: [ 8 | 'src/**/*.ts', 9 | 'test/**/*.ts' 10 | ], 11 | tasks: ['clean:ts', 'ts:default', 'concat:build', 'concat:test', 'concat:coverage', 'buildCoverage'] 12 | } 13 | }; -------------------------------------------------------------------------------- /grunt/clean.js: -------------------------------------------------------------------------------- 1 | // Empties folders to start fresh 2 | module.exports = { 3 | coverage: [ 4 | 'coverage/*' 5 | ], 6 | instrument: [ 7 | 'out/instrument/*' 8 | ], 9 | ts: [ 10 | 'out/instrument/*', 11 | 'out/*.js*', 12 | 'out/src/*', 13 | 'out/test/*' 14 | ], 15 | build: [ 16 | 'out/build.js*' 17 | ], 18 | default: [ 19 | 'out/*' 20 | ] 21 | }; -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "e53e146af47a5301066c9066ed3560a21a7006a9" 10 | }, 11 | "mocha/mocha.d.ts": { 12 | "commit": "e53e146af47a5301066c9066ed3560a21a7006a9" 13 | }, 14 | "sinon/sinon.d.ts": { 15 | "commit": "cf7c97b2a68a385c98c75fb6edd81083c97c983c" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/exercise1/word.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Exercise1 { 4 | // REMOVE THESE LINES WHEN START THE EXERCISE 5 | /* istanbul ignore next */ 6 | export class Word { 7 | constructor(private word:string) { 8 | 9 | } 10 | 11 | public removeVowels():string { 12 | return this.word.replace(/[aeiou]/ig, ''); 13 | } 14 | 15 | public removeNumbers():string { 16 | return 'incomplete method should remove 0-9'; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/exercise4/run.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // THIS FILE IS IGNORED BY TESTS 4 | // Avoid placing logic here! 5 | console.log('\n**** GAME INITIALIZING ****\n'); 6 | 7 | module GameOfLife { 8 | var engine = new Engine(); 9 | var game = { 10 | cycle: function(pipe) { 11 | pipe.print('My example output, the datetime:\n' + 12 | Date() + 13 | '\n\nPress q to quit'); 14 | } 15 | }; 16 | engine.pipe(Terminal.instance()); 17 | engine.game(game); 18 | engine.start(); 19 | } -------------------------------------------------------------------------------- /test/exercise1/word.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Exercise1 { 4 | describe('Exercise1', () => { 5 | describe('Word', () => { 6 | describe('removeVowels()', () => { 7 | // add your tests here (exercise B) 8 | it('should stripe vowels from the word', () => { 9 | 10 | }); 11 | }); 12 | describe('removeNumbers()', () => { 13 | // add your tests here (exercise C) 14 | it('should ... some behavior', () => { 15 | 16 | }); 17 | }); 18 | }); 19 | }); 20 | } -------------------------------------------------------------------------------- /test/exercise3/minesweeper.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module LegacyCode { 4 | describe('LegacyCode', () => { 5 | 6 | describe('MineSweeper', () => { 7 | }); 8 | describe('printMineSweeperBoard', () => { 9 | var consoleStub:SinonStub; 10 | beforeEach(function() { 11 | consoleStub = sandbox.stub(console, 'log') 12 | }); 13 | it('should print a board that signifies legacy behavior is intact', () => { 14 | printMineSweeperBoard([{x: 1, y: 1}, {x: 2, y: 1}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}], 10); 15 | assert.ok(consoleStub.called); 16 | }); 17 | }); 18 | }); 19 | } -------------------------------------------------------------------------------- /grunt/mochaTest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: { 3 | options: { 4 | reporter: 'spec', 5 | quiet: false // Optionally suppress output to standard out (defaults to false) 6 | ,bail: false 7 | }, 8 | src: [ 9 | 'out/test.js' 10 | ] 11 | }, 12 | coverage: { 13 | options: { 14 | // alternate usable values: progress, spec, dot, nyan, landing 15 | reporter: 'landing', 16 | quiet: false, // Optionally suppress output to standard out (defaults to false) 17 | clearRequireCache: true, 18 | bail: false 19 | }, 20 | src: [ 21 | 'out/coverage.js' 22 | ] 23 | } 24 | }; -------------------------------------------------------------------------------- /grunt/replace.js: -------------------------------------------------------------------------------- 1 | // Empties folders to start fresh 2 | module.exports = { 3 | coverage: { 4 | src: ['out/src/**/*.js'], // source files array (supports minimatch) 5 | overwrite: true, // overwrite matched source files 6 | replacements: [ 7 | { 8 | // })(MechanicalThings || (MechanicalThings = {})); 9 | from: /^(\s*)}\)\((\w+) \|\| \(\2 = \{\}\)\);$/gm, 10 | to: '})($2 || /* istanbul ignore next */ ($2 = {}));' 11 | }, 12 | { 13 | // })(MechanicalThings = Example.MechanicalThings || (Example.MechanicalThings = {})); 14 | from: /^(\s*)}\)\((\w+) = (\w+\.\2) \|\| \(\3 = \{\}\)\);$/gm, 15 | to: '})($2 = $3 || /* istanbul ignore next */ ($3 = {}));' 16 | } 17 | ] 18 | } 19 | }; -------------------------------------------------------------------------------- /src/example/MechanicalThings/car.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Example { 4 | export module MechanicalThings { 5 | export class CarImpl implements Transportation { 6 | private x:number = 0; 7 | private y:number = 0; 8 | 9 | sound():string { 10 | return "vroom!"; 11 | } 12 | 13 | position():Coordinate { 14 | return { 15 | x: this.x, 16 | y: this.y 17 | }; 18 | } 19 | 20 | velocity() { 21 | return 1; 22 | } 23 | 24 | move(offset:Coordinate) { 25 | this.x = this.x + (offset.x * this.velocity()); 26 | this.y = this.y + (offset.y * this.velocity()); 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/example/MechanicalThings/plane.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Example { 4 | export module MechanicalThings { 5 | export class PlaneImpl implements Transportation { 6 | private x:number = 0; 7 | private y:number = 10; 8 | 9 | sound():string { 10 | return "vroooooooom!"; 11 | } 12 | 13 | position():Coordinate { 14 | return { 15 | x: this.x, 16 | y: this.y 17 | }; 18 | } 19 | 20 | velocity() { 21 | return 10; 22 | } 23 | 24 | move(offset:Coordinate) { 25 | this.x = this.x + (offset.x * this.velocity()); 26 | this.y = this.y + (offset.y * this.velocity()); 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/example/driver.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Example { 4 | export class Driver { 5 | private coordinate:Coordinate = {x: 0, y: 0}; 6 | 7 | constructor(private vehicle:MechanicalThings.Transportation) { 8 | 9 | } 10 | 11 | park() { 12 | var position = this.vehicle.position(); 13 | this.vehicle.move({x: -position.x / this.vehicle.velocity(), y: -position.y / this.vehicle.velocity()}); 14 | } 15 | 16 | goForward() { 17 | this.vehicle.move({x: 0, y: 1}); 18 | } 19 | 20 | goBackwards() { 21 | this.vehicle.move({x: 0, y: -1}); 22 | } 23 | 24 | turnLeft() { 25 | this.vehicle.move({x: -1, y: 0}); 26 | } 27 | 28 | turnRight() { 29 | this.vehicle.move({x: 1, y: 0}); 30 | } 31 | 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /grunt/concat.js: -------------------------------------------------------------------------------- 1 | // Empties folders to start fresh 2 | module.exports = { 3 | options: { 4 | separator: '\n;\n', 5 | sourceMap: true, 6 | sourceMapStyle: 'inline' 7 | }, 8 | test: { 9 | src: [ 10 | 'out/src/**/*.js', 11 | '!out/src/exercise4/run.js', 12 | 'out/test/index.js', 13 | 'out/test/**/*.js' 14 | ], 15 | dest: 'out/test.js' 16 | }, 17 | build: { 18 | src: [ 19 | 'out/src/**/*.js', 20 | // exclude it first, then re-include it 21 | '!out/src/exercise4/run.js', 22 | 'out/src/exercise4/run.js' 23 | ], 24 | dest: 'out/build.js' 25 | }, 26 | coverage: { 27 | src: [ 28 | 'out/instrument/**/*.js', 29 | '!out/instrument/out/src/exercise4/run.js', 30 | 'out/test/**/*.js' 31 | ], 32 | dest: 'out/coverage.js' 33 | } 34 | }; -------------------------------------------------------------------------------- /src/exercise2/calculator.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Calculator { 4 | export interface Expression { 5 | eval: () => number 6 | } 7 | 8 | export class NumberExpression implements Expression { 9 | constructor(private val:number) { 10 | 11 | } 12 | 13 | eval() { 14 | return this.val 15 | } 16 | } 17 | 18 | export class AddExpression implements Expression { 19 | constructor(private left:Expression, private right:Expression) { 20 | 21 | } 22 | 23 | eval() { 24 | return this.left.eval() + this.right.eval() 25 | } 26 | } 27 | 28 | // REMOVE THESE LINES WHEN START THE EXERCISE 29 | /* istanbul ignore next */ 30 | export class ExpressionComparer { 31 | constructor(private left:Expression, private right:Expression) { 32 | 33 | } 34 | 35 | equals():Boolean { 36 | return this.left.eval() == this.right.eval() 37 | } 38 | 39 | greaterThan():Boolean { 40 | return this.left.eval() < this.right.eval() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/exercise3/minesweeper.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module LegacyCode { 4 | export interface MineCoordinate { 5 | x: number 6 | y: number 7 | } 8 | 9 | /* istanbul ignore next */ 10 | export var printMineSweeperBoard = function (guesses: Array, mineCount:number) { 11 | console.log('number of mines: ' + mineCount); 12 | console.log(''); 13 | var placed = 0; 14 | var x = 10; 15 | var y = 10; 16 | for (var i = 1; i <= y; i++) { 17 | var output = ''; 18 | for (var j = 1; j <= x; j++) { 19 | if (placed < mineCount && (Math.random() > 0.5)) { 20 | placed++; 21 | var mineGuessed = false; 22 | for (var k = 0; k < guesses.length; k++) { 23 | if(guesses[k].x == j && guesses[k].y == i) { 24 | mineGuessed = true; 25 | } 26 | } 27 | output += mineGuessed ? '*' : '?'; 28 | } else { 29 | output += '_'; 30 | } 31 | } 32 | console.log(output); 33 | } 34 | }; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /typings/blessed.d.ts: -------------------------------------------------------------------------------- 1 | declare module GameOfLife { 2 | export interface Blessed { 3 | // Properties 4 | 5 | // Methods 6 | box(options?:Object): BlessedBox 7 | screen(options?:Object): BlessedScreen 8 | program(): BlessedProgram 9 | } 10 | 11 | export interface BlessedElement { 12 | } 13 | export interface BlessedBox extends BlessedElement { 14 | content: string 15 | } 16 | 17 | export interface BlessedScreen { 18 | render(): void 19 | append(BlessedElement): void 20 | remove(BlessedElement): void 21 | enableKeys(): void; 22 | } 23 | 24 | export interface BlessedKeyListenerKey { 25 | name: string 26 | full: string 27 | shift: boolean 28 | 29 | ctrl: boolean 30 | ch: string 31 | } 32 | 33 | export interface BlessedProgram { 34 | key(event:string, handler:(ch:string, key:BlessedKeyListenerKey) => void): void; 35 | unkey(event:string, handler:(ch:string, key:BlessedKeyListenerKey) => void): void; 36 | clear(): void; 37 | enableMouse(): void; 38 | disableMouse(): void 39 | showCursor(): void; 40 | hideCursor(): void; 41 | normalBuffer(): void; 42 | alternateBuffer(): void; 43 | exit(code:number): void; 44 | } 45 | } -------------------------------------------------------------------------------- /src/example/README.md: -------------------------------------------------------------------------------- 1 | # This folder contains NO EXERCISES. 2 | 3 | Use the source code found here for some guidance on TypeScript as it relates to this specific testing environment. 4 | 5 | # Overview 6 | 7 | In browsers, JavaScript shares the same scope across all files. The only way to make something private is to place it inside 8 | a closure. However, in Node, you can [require](https://nodejs.org/api/modules.html) contents in from 9 | [exported modules](https://nodejs.org/api/modules.html#modules_module_exports). 10 | 11 | For the sake of simplicity (and since the focus is TDD, not Node), *most of this project does not leverage Node's require 12 | functionality!* Instead, we are compiling all TypeScript files into a single JavaScript file. The only exception is in 13 | some of the external dependency management related to testing frameworks. 14 | 15 | To accomplish namespaces, we will leverage [TypeScript modules](http://www.typescriptlang.org/Handbook#modules). You can 16 | see an example of this in [MechanicalThings/car.ts](./MechanicalThings/car.ts). 17 | 18 | ```typescript 19 | module MechanicalThings { 20 | // .. 21 | } 22 | ``` 23 | 24 | This converts into a closure when compiled to JavaScript. As you would expect, re-using this same module name in another file will 25 | also share the scope between the two files. You can see this interaction in the [Driver.ts test](../../test/example/driver.ts) 26 | 27 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-04-14 using generator-angular 0.8.0 2 | 'use strict'; 3 | 4 | module.exports = function (grunt) { 5 | // Load grunt tasks automatically 6 | grunt.loadNpmTasks('grunt-notify'); 7 | grunt.loadNpmTasks('grunt-istanbul'); 8 | grunt.loadNpmTasks('grunt-istanbul-coverage'); 9 | grunt.loadNpmTasks('grunt-text-replace'); 10 | 11 | require('load-grunt-config')(grunt, { 12 | jitGrunt: { 13 | } 14 | }); 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | grunt.registerTask('buildCoverage', 19 | [ 20 | 'replace:coverage', 21 | 'clean:instrument', 22 | 'instrument', 23 | 'concat:coverage', 24 | 'mochaTest:coverage', 25 | 'clean:coverage', 26 | 'storeCoverage', 27 | 'makeReport', 28 | 'coverage' 29 | ]); 30 | 31 | grunt.registerTask('test', [ 32 | 'clean:default', 33 | 'ts:default', 34 | 'buildCoverage' 35 | ]); 36 | 37 | grunt.registerTask('build', [ 38 | 'clean:build', 39 | 'ts:default', 40 | 'concat:build' 41 | ]); 42 | 43 | grunt.registerTask('install', [ 44 | 'npm-install' 45 | ]); 46 | 47 | grunt.registerTask('default', [ 48 | 'clean:default', 49 | 'ts:default', 50 | 'watch' 51 | ]); 52 | }; -------------------------------------------------------------------------------- /src/exercise4/engine.ts: -------------------------------------------------------------------------------- 1 | ///// 2 | 3 | module GameOfLife { 4 | export interface IPrintable { 5 | print(content: string); 6 | } 7 | 8 | export interface IGame { 9 | cycle(pipe: IPrintable): void; 10 | } 11 | 12 | export class Engine implements IPrintable { 13 | private refreshInterval:number = 250; 14 | private intervalId:number; 15 | private gameInstance: IGame; 16 | private pipeInstance: IPrintable; 17 | 18 | public pipe(pipe: IPrintable) { 19 | this.pipeInstance = pipe; 20 | return this.pipeInstance 21 | } 22 | 23 | public print(content: string) { 24 | this.pipeInstance.print(content); 25 | } 26 | 27 | public game(game: IGame) { 28 | this.gameInstance = game; 29 | } 30 | 31 | public start() { 32 | if(this.gameInstance) { 33 | this.intervalId = setInterval(this.gameInstance.cycle.bind(this.gameInstance, this.pipeInstance), this.refreshInterval); 34 | } 35 | } 36 | 37 | public stop() { 38 | clearInterval(this.intervalId); 39 | this.intervalId = null; 40 | } 41 | 42 | public getIntervalId() { 43 | return this.intervalId; 44 | } 45 | 46 | public setRefreshRate(interval:number) { 47 | this.refreshInterval = interval; 48 | } 49 | 50 | public getRefreshRate() { 51 | return this.refreshInterval; 52 | } 53 | 54 | } 55 | } 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /references.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | /////////////////////////////////////////////////////////////// 5 | ///// place dependencies that are sensitive to order here ///// 6 | /////////////////////////////////////////////////////////////// 7 | 8 | 9 | /////////////////////////////////////////////////////////////// 10 | /////////////////////////////////////////////////////////////// 11 | 12 | //grunt-start 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | //grunt-end 34 | 35 | -------------------------------------------------------------------------------- /test/example/MechanicalThings/car.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // When testing modules you can declare tests inside a module block 4 | module Example { 5 | export module MechanicalThings { 6 | describe('Example.MechanicalThings', () => { 7 | describe('CarImpl', () => { 8 | describe('move()', () => { 9 | it('should return the right sound effect', () => { 10 | var car = new CarImpl(); 11 | assert.equal(car.sound(), 'vroom!'); 12 | }); 13 | }); 14 | 15 | describe('position()', () => { 16 | it('should return an instance of Coordinate initialized to (0, 0)', () => { 17 | var car = new CarImpl(); 18 | assert.deepEqual({x: 0, y: 0}, car.position()); 19 | }); 20 | }); 21 | 22 | describe('move()', () => { 23 | it('should change the location of position()', () => { 24 | var car = new CarImpl(); 25 | car.move({x: 2, y: 3}); 26 | assert.deepEqual({x: 2, y: 3}, car.position()); 27 | }); 28 | it('should change the location in an additive way', () => { 29 | var car = new CarImpl(); 30 | car.move({x: 2, y: 3}); 31 | car.move({x: 0, y: -4}); 32 | assert.deepEqual({x: 2, y: -1}, car.position()); 33 | }); 34 | }); 35 | }); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-tdd-exercises", 3 | "version": "1.0.0", 4 | "description": "A project to learn TDD in TypeScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "preinstall": "npm install -g typescript mocha tsd@next karma-cli grunt-cli", 8 | "postinstall": "tsd reinstall --save && grunt test", 9 | "pree4": "grunt build", 10 | "e4": "node ./out/build.js", 11 | "ts": "grunt ts", 12 | "test": "grunt test", 13 | "watch": "grunt watch" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/michikono/typescript-tdd-exercises.git" 18 | }, 19 | "keywords": [ 20 | "typescript", 21 | "tdd", 22 | "testing" 23 | ], 24 | "author": "Michi Kono (https://github.com/michikono)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/michikono/typescript-tdd-exercises/issues" 28 | }, 29 | "homepage": "https://github.com/michikono/typescript-tdd-exercises", 30 | "devDependencies": { 31 | "blanket": "^1.1.6", 32 | "growl": "^1.8.1", 33 | "grunt": "^0.4.5", 34 | "grunt-contrib-clean": "^0.6.0", 35 | "grunt-contrib-concat": "^0.5.1", 36 | "grunt-contrib-watch": "^0.6.1", 37 | "grunt-istanbul": "^0.5.0", 38 | "grunt-istanbul-coverage": "^0.1.1", 39 | "grunt-mocha-test": "^0.12.7", 40 | "grunt-notify": "^0.4.1", 41 | "grunt-text-replace": "^0.4.0", 42 | "grunt-ts": "^3.0.0", 43 | "karma-mocha": "^0.1.10", 44 | "load-grunt-config": "^0.16.0", 45 | "mocha": "^2.2.4", 46 | "sinon": "^1.12.2", 47 | "source-map-support": "^0.2.10", 48 | "time-grunt": "^1.1.0" 49 | }, 50 | "dependencies": { 51 | "blessed": "^0.1.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/example/MechanicalThings/plane.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Alternate testing method of modules -- explicitly typing out the entire namespace 4 | module Example { 5 | describe('Example.MechanicalThings Module', () => { 6 | describe('PlaneImpl', () => { 7 | describe('move()', () => { 8 | it('should return the right sound effect', () => { 9 | var plane = new Example.MechanicalThings.PlaneImpl(); 10 | assert.equal(plane.sound(), 'vroooooooom!'); 11 | }); 12 | }); 13 | 14 | describe('position()', () => { 15 | it('should return an instance of Coordinate initialized to (0, 10) (because planes are in the sky!)', () => { 16 | var plane = new Example.MechanicalThings.PlaneImpl(); 17 | assert.deepEqual({x: 0, y: 10}, plane.position()); 18 | }); 19 | }); 20 | 21 | describe('move()', () => { 22 | it('should change the location of position() by a factor of 10 (because planes are fast!)', () => { 23 | var plane = new Example.MechanicalThings.PlaneImpl(); 24 | plane.move({x: 2, y: 3}); 25 | assert.deepEqual({x: 20, y: 40}, plane.position()); 26 | }); 27 | it('should change the location in an additive way', () => { 28 | var plane = new Example.MechanicalThings.PlaneImpl(); 29 | plane.move({x: 2, y: 3}); 30 | plane.move({x: 0, y: -4}); 31 | assert.deepEqual({x: 20, y: 0}, plane.position()); 32 | }); 33 | }); 34 | }); 35 | }); 36 | } -------------------------------------------------------------------------------- /test/exercise2/calculator.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module Calculator { 4 | describe('Exercise2', () => { 5 | describe('Calculator Module', () => { 6 | describe('NumberExpression', () => { 7 | it('should return the number provided in the constructor', () => { 8 | var expression = new NumberExpression(5); 9 | assert.ok(expression.eval() === 5); 10 | }); 11 | }); 12 | describe('AddExpression', () => { 13 | it('should return the sum of two Expressions provided in the constructor', () => { 14 | var expression = new AddExpression(new NumberExpression(2), new NumberExpression(-5)); 15 | assert.ok(expression.eval() === -3) 16 | }); 17 | }); 18 | describe('ExpressionComparer', () => { 19 | describe('equals', () => { 20 | it('should return true if the two expression evaluations are equal', () => { 21 | var expression = new ExpressionComparer(new NumberExpression(35), new NumberExpression(35)); 22 | assert.ok(expression.equals()); 23 | }); 24 | it('should return false if the two expression evaluations are not equal', () => { 25 | }); 26 | }); 27 | describe('greaterThan', () => { 28 | it('should return true if the left expression is greater than the right', () => { 29 | }); 30 | it('should return false if the left expression is equal to or less than the right', () => { 31 | }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | } -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | require('source-map-support').install(); 3 | 4 | module Example { 5 | export var assert = require("assert"); 6 | export var sinon:SinonStatic = require("sinon"); 7 | export var sandbox:SinonSandbox; 8 | 9 | beforeEach(() => { 10 | sandbox = sinon.sandbox.create(); 11 | }); 12 | 13 | afterEach(() => { 14 | sandbox.restore(); 15 | }); 16 | } 17 | 18 | module Exercise1 { 19 | export var assert = require("assert"); 20 | export var sinon:SinonStatic = require("sinon"); 21 | export var sandbox:SinonSandbox; 22 | 23 | beforeEach(() => { 24 | sandbox = sinon.sandbox.create(); 25 | }); 26 | 27 | afterEach(() => { 28 | sandbox.restore(); 29 | }); 30 | } 31 | 32 | module Calculator { 33 | export var assert = require("assert"); 34 | export var sinon:SinonStatic = require("sinon"); 35 | export var sandbox:SinonSandbox; 36 | 37 | beforeEach(() => { 38 | sandbox = sinon.sandbox.create(); 39 | }); 40 | 41 | afterEach(() => { 42 | sandbox.restore(); 43 | }); 44 | } 45 | 46 | module LegacyCode { 47 | export var assert = require("assert"); 48 | export var sinon:SinonStatic = require("sinon"); 49 | export var sandbox:SinonSandbox; 50 | 51 | beforeEach(() => { 52 | sandbox = sinon.sandbox.create(); 53 | }); 54 | 55 | afterEach(() => { 56 | sandbox.restore(); 57 | }); 58 | } 59 | module GameOfLife { 60 | export var assert = require("assert"); 61 | export var sinon:SinonStatic = require("sinon"); 62 | export var sandbox:SinonSandbox; 63 | 64 | beforeEach(() => { 65 | sandbox = sinon.sandbox.create(); 66 | }); 67 | 68 | afterEach(() => { 69 | sandbox.restore(); 70 | }); 71 | 72 | // purposely destroy the local instance of this variable 73 | // too many bugs arise from it being accidentally invoked 74 | blessed = { 75 | program: null, 76 | screen: null, 77 | box: null 78 | }; 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/exercise1/README.md: -------------------------------------------------------------------------------- 1 | # Introduction to TDD & Unit Testing 2 | 3 | ## Objectives 4 | 5 | 1. Learn the tools 6 | 2. Write a test on existing code 7 | 3. Add tests 8 | 9 | ## Exercise A 10 | 11 | Find and open the [/coverage/reports/lcov-report/index.html](../../coverage/reports/lcov-report/index.html) and navigate 12 | the file. 13 | 14 | You can re-run tests individually by running: 15 | 16 | ```shell 17 | $ grunt test 18 | ``` 19 | 20 | This command will also refresh the coverage report. Note that the you can run these commands automatically via the 21 | `watch` command: 22 | 23 | ```shell 24 | $ grunt watch 25 | > 26 | > Running "watch" task 27 | > Waiting... 28 | ``` 29 | 30 | This is the baseline you want for all of your tests going forward. 31 | 32 | As you make any changes to TypeScript files, you should see this command trigger events automatically. For example, 33 | try adding a space to [/src/exercise1/word.ts](../../src/exercise1/word.ts) and save it. You will see the `watch` 34 | recompile the TypeScript and run all tests. 35 | 36 | ## Exercise B 37 | 38 | Right now, we actually do not have 100% coverage; some test coverage analysis is being suppressed. Edit 39 | [word.ts](./word.ts); remove the line about `istanbul ignore` that looks like this: 40 | 41 | ```typescript 42 | /* istanbul ignore next */ 43 | ``` 44 | 45 | By removing this line, the class defined below it will begin to count against the code coverage reports. You should notice 46 | your code coverage is no longer at 100%. 47 | 48 | Add a test for the `removeVowels()` method. The test should go to [/test/exercise1/word.ts](../../test/exercise1/word.ts) 49 | 50 | You should see the coverage report edge back up to ~90%. The 51 | [exercise 1 coverage report](../../coverage/reports/lcov-report/exercise1/word.js.html) should show the removeVowels 52 | method fully covered (green). But you will notice `removeNumbers()` is still red and it is causing coverage to fall below 100%. 53 | 54 | ## Exercise C 55 | 56 | Add a test for the `removeNumbers()` method. The test should go to [/test/exercise1/word.ts](../../test/exercise1/word.ts) 57 | 58 | Note that you want to write the test, *that will fail*. This is because we have not yet added the logic for `removeNumbers()`. 59 | *Make sure it fails first.* 60 | 61 | After you've written the test, update the `removeNumbers()` method so that it has the appropriate logic. Your test 62 | should pass. 63 | 64 | You are done when all tests pass and coverage is 100%. 65 | -------------------------------------------------------------------------------- /src/exercise2/README.md: -------------------------------------------------------------------------------- 1 | # Spies, Mocks, & Stubs 2 | 3 | ## Objectives 4 | 5 | 1. Learn the difference between spies, mocks, and stubs 6 | 2. Write tests for code with dependencies 7 | 8 | ## Exercise A 9 | 10 | In this exercise, you are given some code that already has partial coverage. 11 | 12 | Edit [calculator.ts](./calculator.ts); remove the lines about `istanbul ignore` on **line 28 & 29** that look like this: 13 | 14 | ```typescript 15 | /* istanbul ignore next */ 16 | ``` 17 | 18 | By removing these lines, the class defined below it will begin to count against the code coverage reports. You should notice 19 | your code coverage is no longer at 100%. Make sure your watcher is running (`grunt watch`) 20 | 21 | 1. Implement the remaining tests and fix any bugs you find in the process. 22 | 2. Add `NegateExpression` (`-n`) - inverts the provided `Expression`. For example, `NegateExpression(NumberExpression(-3))` 23 | would evaluate to 3. 24 | 3. Add `PowerExpression` (`2^n`) - takes the given `Expression` and sets it as the exponent of 2. For example, 25 | `PowerExpression(NumberExpression(3))` would evaluate to 8 26 | 4. Make sure your code coverage is constantly at 100%. 27 | 28 | ## Exercise B 29 | 30 | In most of your previous tests, you probably used the `NumberExpression` class. This might make 31 | sense because in this case, `NumberExpression` is very straight forward. The problem with this approach 32 | is that if there is a bug in `NumberExpression`, all of your subsequent tests break. 33 | 34 | Replace the contents of `NumberExpression`.`eval()` so it always returns a random number between 1 - 3: 35 | 36 | ```typescript 37 | Math.ceil(Math.random() * 3) 38 | ``` 39 | 40 | Notice how most of your tests are now broken, and inconsistently at that! Fix all your tests so that each 41 | passes regardless of what `Expression` implementation is passed into it. Try approaches varying 42 | between mocks, stubs, and dummy implementations of `Expression`. When done, revert the "bug" introduced to 43 | `NumberExpression`.`eval()`. 44 | 45 | ## Exercise C 46 | 47 | In this last example, we will examine using external dependencies. In this specific case, we will treat 48 | JavaScript standard library `Math` as one: you can't modify it, didn't write it, but need its behavior. 49 | 50 | Add the following new `Expression` type and write tests for it: 51 | 52 | * `RandomExpression` (`random(n)`) - return a random integer between 0 and `Expression`, inclusively. For example, 53 | `RandomExpression(NumberExpression(3))` would evaluate to a random number 0 - 3 54 | 55 | Note that this last `Expression` implementation's tests will absolutely require that you stub the `Math.random` method. 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmp.txt 2 | tmp/* 3 | typings/* 4 | !typings/blessed.d.ts 5 | src/*.js* 6 | test/*.js* 7 | src/**/*.js* 8 | test/**/*.js* 9 | 10 | # Created by .ignore support plugin (hsz.mobi) 11 | ### SublimeText template 12 | # cache files for sublime text 13 | *.tmlanguage.cache 14 | *.tmPreferences.cache 15 | *.stTheme.cache 16 | 17 | # workspace files are user-specific 18 | *.sublime-workspace 19 | 20 | # project files should be checked into the repository, unless a significant 21 | # proportion of contributors will probably not be using SublimeText 22 | # *.sublime-project 23 | 24 | # sftp configuration file 25 | sftp-config.json 26 | 27 | 28 | ### JetBrains template 29 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 30 | 31 | *.iml 32 | 33 | ## Directory-based project format: 34 | .idea/ 35 | # if you remove the above rule, at least ignore the following: 36 | 37 | # User-specific stuff: 38 | # .idea/workspace.xml 39 | # .idea/tasks.xml 40 | # .idea/dictionaries 41 | 42 | # Sensitive or high-churn files: 43 | # .idea/dataSources.ids 44 | # .idea/dataSources.xml 45 | # .idea/sqlDataSources.xml 46 | # .idea/dynamic.xml 47 | # .idea/uiDesigner.xml 48 | 49 | # Gradle: 50 | # .idea/gradle.xml 51 | # .idea/libraries 52 | 53 | # Mongo Explorer plugin: 54 | # .idea/mongoSettings.xml 55 | 56 | ## File-based project format: 57 | *.ipr 58 | *.iws 59 | 60 | ## Plugin-specific files: 61 | 62 | # IntelliJ 63 | out/ 64 | 65 | # mpeltonen/sbt-idea plugin 66 | .idea_modules/ 67 | 68 | # JIRA plugin 69 | atlassian-ide-plugin.xml 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | 76 | 77 | ### OSX template 78 | .DS_Store 79 | .AppleDouble 80 | .LSOverride 81 | 82 | # Icon must end with two \r 83 | Icon 84 | 85 | # Thumbnails 86 | ._* 87 | 88 | # Files that might appear on external disk 89 | .Spotlight-V100 90 | .Trashes 91 | 92 | # Directories potentially created on remote AFP share 93 | .AppleDB 94 | .AppleDesktop 95 | Network Trash Folder 96 | Temporary Items 97 | .apdisk 98 | 99 | 100 | ### Node template 101 | # Logs 102 | logs 103 | *.log 104 | 105 | # Runtime data 106 | pids 107 | *.pid 108 | *.seed 109 | 110 | # Directory for instrumented libs generated by jscoverage/JSCover 111 | lib-cov 112 | 113 | # Coverage directory used by tools like istanbul 114 | coverage 115 | 116 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 117 | .grunt 118 | 119 | # node-waf configuration 120 | .lock-wscript 121 | 122 | # Compiled binary addons (http://nodejs.org/api/addons.html) 123 | build/Release 124 | 125 | # Dependency directory 126 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 127 | node_modules 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/exercise4/terminal.ts: -------------------------------------------------------------------------------- 1 | ///// 2 | 3 | module GameOfLife { 4 | export var blessed:Blessed = require('blessed'); 5 | 6 | export class Terminal implements IPrintable { 7 | private blessed:Blessed; 8 | private program:BlessedProgram; 9 | private screen:BlessedScreen; 10 | private box:BlessedBox = { 11 | content: '' 12 | }; 13 | 14 | constructor(blessed?:Blessed) { 15 | // a more testable variation of this is to call methods on whatever 16 | // was passed in 17 | if(blessed) { 18 | this.setBlessed(blessed); 19 | } 20 | } 21 | 22 | public static instance():Terminal { 23 | var terminal = new GameOfLife.Terminal(); 24 | terminal.setBlessed(blessed); 25 | return terminal; 26 | } 27 | 28 | public print(content: string) { 29 | this.setContent(content); 30 | } 31 | 32 | public setContent(content:string) { 33 | this.screen.render(); 34 | this.box.content = content; 35 | } 36 | 37 | public getContent() { 38 | return this.box.content; 39 | } 40 | 41 | public exit() { 42 | /* istanbul ignore else */ 43 | if (this.program) { 44 | this.program.clear(); 45 | this.program.showCursor(); 46 | this.program.normalBuffer(); 47 | } 48 | this.killProcess() 49 | } 50 | 51 | // note: this probably belongs in another class 52 | public setBlessed(blessed:Blessed) { 53 | this.program = blessed.program(); 54 | this.program.key('q', this.getQuitCallback()); 55 | this.program.clear(); 56 | 57 | this.screen = blessed.screen({ 58 | program: this.program, 59 | autoPadding: true, 60 | smartCSR: true 61 | }); 62 | this.screen.enableKeys(); 63 | // this contains the initial content 64 | this.box = blessed.box({ 65 | top: '0', 66 | left: '0', 67 | width: '100%', 68 | height: '100%', 69 | content: '', 70 | tags: true, 71 | style: { 72 | fg: '#ffffff', 73 | bg: '#000000' 74 | } 75 | }); 76 | this.screen.append(this.box); 77 | } 78 | 79 | /* istanbul ignore next */ 80 | private getQuitCallback() { 81 | return ((ch, key) => { 82 | this.exit() 83 | }).bind(this); 84 | } 85 | 86 | private killProcess() { 87 | this.program.unkey('q', this.getQuitCallback()); 88 | process.exit(0); 89 | } 90 | } 91 | } 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/exercise4/README.md: -------------------------------------------------------------------------------- 1 | # Writing a Real Program 2 | 3 | ## Objectives 4 | 5 | 1. Deal with external dependencies/frameworks 6 | 2. Use lessons learned to write a larger program 7 | 8 | ## Summary 9 | 10 | In this exercise, we will write a traditional TDD exercise application: Conway's Game of Life. This game has a few simple 11 | rules that simulate "life." 12 | 13 | To help you build this game, this exercise comes with a basic framework that handles some of the lower level display logic. 14 | There is a `Terminal` class that you can use to print content to the screen. It is used like this (you don't need to test 15 | this out): 16 | 17 | ```typescript 18 | var terminal = Terminal.instance(); 19 | terminal.setContent("text to show") 20 | ``` 21 | 22 | You can press "q" to quit the program. 23 | 24 | A skeleton game engine has also been written for you. Use it like this: 25 | 26 | ```typescript 27 | var engine = new Engine(); 28 | engine.pipe(printer: IPrintable); 29 | engine.game(game: IGame); 30 | engine.start(); 31 | ``` 32 | 33 | The `IGame` instance has a `cycle` method that is called every `engine.getRefreshRate()` milliseconds (default: 250). In 34 | that method, use the `IPrintable` argument's `print` method to push contents to the screen. 35 | 36 | # Testing Tips 37 | 38 | * Make sure you stub or mock the `Terminal` in your game tests. The `Terminal` represents an external dependency which 39 | you never want to call in your tests. 40 | * Write your tests WITH your code; not necessarily beforehand, but definitely don't wait until the end 41 | * If something is difficult to test, re-think what you've written. 42 | [Inversion of control](http://stackoverflow.com/questions/3058/what-is-inversion-of-control) is usually a good place to 43 | start. 44 | * Mocking/stubbing/spying-on instance methods called during a constructor is sometimes challenging, since it will impact 45 | all subsequent instances unless you are careful. If you plan to do this, use this syntax (`sandbox` ensures your hooks get cleaned up): 46 | 47 | ```typescript 48 | var mySpy = sandbox.spy(TheClassName.prototype, 'instanceMethod'); 49 | ``` 50 | 51 | 52 | # Rules of the Game 53 | 54 | [Conway's Game of Life](http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) is a simple game with four rules: 55 | 56 | 1. Any live cell with fewer than two live neighbours dies, as if caused by under-population. 57 | 2. Any live cell with two or three live neighbours lives on to the next generation. 58 | 3. Any live cell with more than three live neighbours dies, as if by overcrowding. 59 | 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. 60 | 61 | Build this game with a board size of your choosing. You can use spaces for empty cells and any other character for 62 | living ones (for example, this one: █). You may need to create more files and classes to do this elegantly. The 63 | `grunt watch` process will detect these new files without problem. 64 | 65 | # Running your Game Outside of Tests 66 | 67 | To run your game, initialize your game logic in [run.ts](./run.ts). There is already a example in that file. To run it: 68 | 69 | ```shell 70 | $ npm run e4 71 | ``` 72 | 73 | You can press "q" to quit the program. 74 | 75 | The current sample code in [`run.ts`](./run.ts) is using a simple object that matches the `IGame` interface. Because 76 | `run.ts` is not covered by tests, it is important that it is as small as possible. Try refactoring your version of 77 | this code to use a more elegant design. For example, using a class to wrap the game logic better: 78 | 79 | ```typescript 80 | var engine = new Engine(); 81 | engine.pipe(Terminal.instance()); 82 | engine.game(new MineSweeper()); 83 | engine.start(); 84 | ``` 85 | 86 | or the [Facade Pattern](http://en.wikipedia.org/wiki/Facade_pattern) to hide this initialization logic: 87 | 88 | ```typescript 89 | new MineSweeperGame().start(); 90 | ``` 91 | 92 | You may want to make these types of changes last after you have things working. 93 | -------------------------------------------------------------------------------- /test/exercise4/engine.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module GameOfLife { 4 | describe('GameOfLife', () => { 5 | // it's very important we stub out external dependencies 6 | var engine: Engine; 7 | var clock:SinonFakeTimers; 8 | var sandbox:SinonSandbox; 9 | 10 | beforeEach(() => { 11 | sandbox = sinon.sandbox.create(); 12 | clock = sandbox.useFakeTimers(); 13 | }); 14 | 15 | afterEach(() => { 16 | sandbox.restore(); 17 | }); 18 | 19 | describe('Engine', () => { 20 | it('should exist', () => { 21 | assert.ok(new Engine); 22 | }); 23 | 24 | describe('pipe', () => { 25 | it('should return the pipe instance you pass in', () => { 26 | var pipe = { 27 | print: () => '' 28 | }; 29 | 30 | var engine = new Engine(); 31 | assert.equal(pipe, engine.pipe(pipe)) 32 | }); 33 | }); 34 | 35 | describe('print', () => { 36 | it('should return the pipe instance you pass in', () => { 37 | var pipe = { 38 | print: sandbox.spy() 39 | }; 40 | var engine = new Engine(); 41 | engine.pipe(pipe); 42 | engine.print('changes'); 43 | assert.ok(pipe.print.called); 44 | }); 45 | }); 46 | 47 | describe('game', () => { 48 | var mockGame: IGame; 49 | beforeEach(() => { 50 | mockGame = { 51 | cycle: sandbox.spy() 52 | } 53 | }); 54 | 55 | it('should not call cycle initially', () => { 56 | engine = new Engine(); 57 | engine.game(mockGame); 58 | engine.start(); 59 | assert.ok(( mockGame.cycle).notCalled); 60 | }); 61 | it('should call cycle once after the initial tick', () => { 62 | engine = new Engine(); 63 | engine.game(mockGame); 64 | engine.start(); 65 | clock.tick(engine.getRefreshRate() + 1); 66 | assert.ok(( mockGame.cycle).calledOnce); 67 | }); 68 | it('should call cycle multiple times as time moves forward', () => { 69 | engine = new Engine(); 70 | engine.game(mockGame); 71 | engine.start(); 72 | clock.tick(engine.getRefreshRate() + 1); 73 | clock.tick(engine.getRefreshRate() + 1); 74 | assert.ok(( mockGame.cycle).calledTwice); 75 | }); 76 | it('should call cycle at a rate that is set by setRefreshRate', () => { 77 | engine = new Engine(); 78 | engine.game(mockGame); 79 | engine.setRefreshRate(1000); 80 | engine.start(); 81 | clock.tick(1001); 82 | 83 | clock.tick(1001); 84 | assert.ok(( mockGame.cycle).called); 85 | }); 86 | }); 87 | 88 | describe('start', () => { 89 | var mockGame: IGame; 90 | beforeEach(() => { 91 | mockGame = { 92 | cycle: sandbox.spy() 93 | } 94 | }); 95 | 96 | it('should bind game.cycle to game and he pipe instance', () => { 97 | var pipe = { 98 | print: sandbox.spy() 99 | }; 100 | engine = new Engine(); 101 | engine.game(mockGame); 102 | engine.pipe(pipe); 103 | var bindSpy = sandbox.spy(mockGame.cycle, 'bind'); 104 | engine.start(); 105 | assert.ok(bindSpy.calledWithExactly(mockGame, pipe)); 106 | }); 107 | }); 108 | describe('stop', () => { 109 | it('should get rid of the interval ID', () => { 110 | engine = new Engine(); 111 | engine.start(); 112 | engine.stop(); 113 | assert.ok(!engine.getIntervalId()); 114 | }); 115 | }); 116 | 117 | describe('setRefreshRate', () => { 118 | it('should change the internal refresh rate (via getRefreshRate)', () => { 119 | engine = new Engine(); 120 | var customRefreshRate = 600; 121 | assert.notEqual(customRefreshRate, engine.getRefreshRate()); 122 | engine.setRefreshRate(customRefreshRate); 123 | assert.equal(customRefreshRate, engine.getRefreshRate()); 124 | }); 125 | }); 126 | }); 127 | }); 128 | }
 129 | -------------------------------------------------------------------------------- /test/example/driver.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // When testing modules you can declare tests inside a module block 4 | module Example { 5 | class MockTransportationImpl implements MechanicalThings.Transportation { 6 | constructor(private x, private y) { 7 | } 8 | 9 | sound() { 10 | return 'mock'; 11 | } 12 | 13 | move(offset:Coordinate) { 14 | this.x = this.x + (offset.x * this.velocity()); 15 | this.y = this.y + (offset.y * this.velocity()); 16 | } 17 | 18 | velocity() { 19 | return 5; 20 | } 21 | 22 | position() { 23 | return {x: this.x, y: this.y}; 24 | } 25 | } 26 | 27 | describe('Example Module', () => { 28 | describe('Driver', () => { 29 | describe('park()', () => { 30 | it('(CUSTOM IMPL) should reset the transportation position to (0, 0)', () => { 31 | var mockTransport = new MockTransportationImpl(2, 3); 32 | var driver = new Driver(mockTransport); 33 | driver.park(); 34 | assert.deepEqual({x: 0, y: 0}, mockTransport.position()); 35 | }); 36 | 37 | // see more on this topic here: http://sinonjs.org/docs/#mocks 38 | // mocks are good for testing dependency objects are used correctly (looking for specific method calls) 39 | it('(USING A MOCK) should reset the transportation position to (0, 0)', () => { 40 | //var mockTransport = new MockTransportationImpl(2, 3); 41 | var car = new MechanicalThings.CarImpl(); 42 | var driver = new Driver(car); 43 | 44 | // register a mock 45 | var mock = sandbox.mock(car); 46 | mock.expects("position").once().returns({x: 0, y: 0}); 47 | mock.expects("move").withArgs({x: 0, y: 0}).once(); 48 | 49 | driver.park(); 50 | 51 | // verify that the expected behavior has executed 52 | mock.verify(); 53 | }); 54 | 55 | // see more on this topic here: http://sinonjs.org/docs/#stubs 56 | // stubs are best for specifying control flow; they overwrite behavior on specific methods 57 | it('(USING A STUB) should reset the transportation position to (0, 0)', () => { 58 | var mockTransport = new MechanicalThings.CarImpl(); 59 | 60 | // setup stubs to overwrite move method to force it to (0, 0) 61 | var stub = sandbox.stub(mockTransport, 'move', function () { 62 | this.x = 1; 63 | this.y = 1; 64 | }); 65 | var driver = new Driver(mockTransport); 66 | 67 | driver.park(); 68 | 69 | // undo the stubs 70 | stub.restore(); 71 | 72 | // should be parked at wherever we forced move to set the car 73 | assert.deepEqual({x: 1, y: 1}, mockTransport.position()); 74 | }); 75 | 76 | // see more on this topic here: http://sinonjs.org/docs/#spies 77 | // spies are best for callbacks, but should be avoided in favor of mocks for objects 78 | // spies still execute the original methods! 79 | it('(USING SPIES) should reset the transportation position to (0, 0)', () => { 80 | var car = new MechanicalThings.CarImpl(); 81 | var positionSpy = sandbox.spy(car, "position"); 82 | var moveSpy = sandbox.spy(car, "move"); 83 | 84 | var driver = new Driver(car); 85 | driver.park(); 86 | 87 | assert.ok(positionSpy.calledOnce); 88 | assert.ok(moveSpy.withArgs({x: 0, y: 0}).calledOnce); 89 | }); 90 | }); 91 | describe('goForward()', () => { 92 | it('should change the position by (0, 1 * velocity)', () => { 93 | var mockTransport = new MockTransportationImpl(2, 3); 94 | var driver = new Driver(mockTransport); 95 | driver.goForward(); 96 | assert.deepEqual({x: 2, y: 8}, mockTransport.position()); 97 | }); 98 | }); 99 | describe('goBackwards()', () => { 100 | it('should change the position by (0, -1 * velocity)', () => { 101 | var mockTransport = new MockTransportationImpl(2, 3); 102 | var driver = new Driver(mockTransport); 103 | driver.goBackwards(); 104 | assert.deepEqual({x: 2, y: -2}, mockTransport.position()); 105 | }); 106 | }); 107 | describe('turnLeft()', () => { 108 | it('should change the position by (-1 * velocity, 0)', () => { 109 | var mockTransport = new MockTransportationImpl(2, 3); 110 | var driver = new Driver(mockTransport); 111 | driver.turnLeft(); 112 | assert.deepEqual({x: -3, y: 3}, mockTransport.position()); 113 | }); 114 | }); 115 | describe('turnRight()', () => { 116 | it('should change the position by (0, 1 * velocity)', () => { 117 | var mockTransport = new MockTransportationImpl(2, 3); 118 | var driver = new Driver(mockTransport); 119 | driver.turnRight(); 120 | assert.deepEqual({x: 7, y: 3}, mockTransport.position()); 121 | }); 122 | }); 123 | }); 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /test/exercise4/terminal.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module GameOfLife { 4 | describe('GameOfLife', () => { 5 | describe('Terminal', () => { 6 | var programMock:BlessedProgram; 7 | var screenMock:BlessedScreen; 8 | var boxMock:BlessedBox; 9 | var processStub:SinonStub; 10 | var terminal:Terminal; 11 | 12 | beforeEach(() => { 13 | blessed.program = sandbox.stub(); 14 | blessed.screen = sandbox.stub(); 15 | blessed.box = sandbox.stub(); 16 | 17 | programMock = { 18 | key: sandbox.stub(), 19 | unkey: sandbox.stub(), 20 | clear: sandbox.stub(), 21 | enableMouse: sandbox.stub(), 22 | disableMouse: sandbox.stub(), 23 | showCursor: sandbox.stub(), 24 | hideCursor: sandbox.stub(), 25 | normalBuffer: sandbox.stub(), 26 | alternateBuffer: sandbox.stub(), 27 | exit: sandbox.stub() 28 | }; 29 | 30 | screenMock = { 31 | render: sandbox.stub(), 32 | append: sandbox.stub(), 33 | remove: sandbox.stub(), 34 | enableKeys: sandbox.stub() 35 | }; 36 | 37 | boxMock = { 38 | content: '' 39 | }; 40 | ( blessed.box).returns(boxMock); 41 | ( blessed.program).returns(programMock); 42 | ( blessed.screen).returns(screenMock); 43 | 44 | processStub = sandbox.stub(process, 'exit'); 45 | }); 46 | 47 | afterEach(() => { 48 | terminal = null; 49 | }); 50 | 51 | describe('Constructor', () => { 52 | it('Should not require any arguments', () => { 53 | terminal = new Terminal(); 54 | assert.ok(terminal); 55 | }); 56 | 57 | it('Should allow blessed to be passed in', () => { 58 | var setBlessedStub = sandbox.stub(Terminal.prototype, 'setBlessed'); 59 | terminal = new Terminal(blessed); 60 | assert.ok(setBlessedStub.calledWithExactly(blessed)); 61 | }); 62 | }); 63 | describe('instance', () => { 64 | it('should return an instance of Terminal', () => { 65 | var spy = sandbox.spy(GameOfLife, 'Terminal'); 66 | var instance = Terminal.instance(); 67 | assert.ok(spy.calledWithNew(spy)); 68 | }); 69 | it('should pass blessed into the constructor call', () => { 70 | var setBlessedStub = sandbox.stub(Terminal.prototype, 'setBlessed'); 71 | var instance = Terminal.instance(); 72 | assert.ok(setBlessedStub.calledWithExactly(blessed)); 73 | }); 74 | it('should call setBlessed', () => { 75 | var setBlessedStub = sandbox.stub(Terminal.prototype, 'setBlessed'); 76 | var instance = Terminal.instance(); 77 | assert.ok(setBlessedStub.called); 78 | }); 79 | }); 80 | describe('setBlessed', () => { 81 | it('should attach a "q" event to getQuitCallback and call clear()', () => { 82 | terminal = new Terminal(); 83 | var getQuitCallbackSpy = sandbox.spy(terminal, 'getQuitCallback'); 84 | terminal.setBlessed(blessed); 85 | assert.ok(( programMock.key).calledWith('q')); 86 | assert.ok(( programMock.clear).calledOnce); 87 | assert.ok(getQuitCallbackSpy.calledOnce); 88 | }); 89 | // this is the main behavior that is crucial to showing content
 90 | it('should append a box object to the screen', () => { 91 | terminal = new Terminal(); 92 | terminal.setBlessed(blessed); 93 | assert.ok(( screenMock.append).calledOnce); 94 | assert.ok(( screenMock.enableKeys).calledOnce); 95 | }); 96 | }); 97 | describe('setContent', () => { 98 | it('should set the internal content', () => { 99 | terminal = new Terminal(blessed); 100 | assert.notEqual("new content", terminal.getContent()); 101 | terminal.setContent("new content"); 102 | assert.equal("new content", terminal.getContent()); 103 | }); 104 | }); 105 | describe('print', () => { 106 | it('should set be a wrapper for setContent', () => { 107 | terminal = new Terminal(blessed); 108 | var setContentStub = sandbox.stub(terminal, 'setContent'); 109 | terminal.print("new content"); 110 | assert.ok(setContentStub.called); 111 | }); 112 | }); 113 | describe('exit', () => { 114 | it('should attempt to kill the process', () => { 115 | var terminal = new Terminal(blessed); 116 | terminal.exit(); 117 | assert.ok(processStub.calledWith(0)); 118 | }); 119 | it('should attempt to kill the process', () => { 120 | var terminal = new Terminal(blessed); 121 | var getQuitCallbackStub = sandbox.stub(terminal, 'getQuitCallback'); 122 | terminal.exit(); 123 | assert.ok(( programMock.unkey).calledWith('q')); 124 | assert.ok(getQuitCallbackStub.calledOnce); 125 | }); 126 | }); 127 | }); 128 | }); 129 | }
 130 | -------------------------------------------------------------------------------- /src/exercise3/README.md: -------------------------------------------------------------------------------- 1 | # Introducing Breaking Changes 2 | 3 | ## Objectives 4 | 5 | 1. Learn strategies for testing legacy code 6 | 2. Learn how to refactor legacy code in a TDD way 7 | 8 | ## Summary 9 | 10 | In Exercise 2B, we touched on how test coverage helps you see regression issues. In this section, we will modify 11 | legacy code by introducing incremental tests. 12 | 13 | Note that for this exercise, the legacy methods will NOT be counted toward test coverage, but all new code you write will. 14 | This will closely resemble how you might add tests in a production code base that lacks tests. 15 | 16 | The legacy code in question places mines onto an imaginary board. We have two issues with this code: add a new feature for 17 | dynamic board size and to fix a bug with mine placement, but either change would require a major rewrite. We will tackle 18 | this rewrite by breaking the method into testable components. 19 | 20 | # Exercise A 21 | 22 | In this first exercise, we want to introduce a small structural change that lets us write our high-level tests. 23 | 24 | Our first objective is to move the legacy function inside an object (class). This first set of changes is the most 25 | risky in that they are made with little to no test coverage. Subsequent changes become less risky as tests are added. 26 | 27 | Examine [minesweeper.ts](./minesweeper.ts). 28 | 29 | Our first change is to separate the printing logic from the calculation logic without impacting dependencies. We will 30 | move the entire contents of `printMineSweeperBoard` to another method to accomplish this. 31 | 32 | * Create a `MineSweeper` class inside the `LegacyCode` module. 33 | * Create a new method called `toString` and move the current contents of `printMineSweeperBoard` into it 34 | * Change the signature of `toString` to return a string that would otherwise be printed to console. 35 | * Change `printMineSweeperBoard` so that it prints the return value from `toString` as shown here: 36 | 37 | ```typescript 38 | console.log(new MineSweeper().toString(guesses, mineCount)); 39 | ``` 40 | 41 | The above steps move key pieces of logic out of the legacy function without disrupting legacy code. 42 | 43 | # Exercise B 44 | 45 | Now we need to add as many tests as we can to the legacy logic while recognizing some things are difficult/impossible to test. 46 | 47 | **There is a bug with how many mines are placed**; we will fix it later so don't worry about finding it if you don't 48 | encounter it. Had this been a real assignment, this may be when we would have caught the issue. For now, ignore all 49 | failing tests that fail inconsistently. When writing tests against mine placement, use a low number of mines being 50 | placed. We will fix this in exercise 3. 51 | 52 | Let's add more tests so that refactoring is safer. Add tests to `toString` that: 53 | 54 | * Checks that the string represents a 10x10 grid (`(10 chars + line break) * 10 rows`) 55 | * Ensures at least `n` mines are placed (mines are either `?` or `*`) where `n` is the argument provided in the method. 56 | You may want to use a small value for `n` for this test given the bug mentioned previously. DO NOT FIX THE BUG. 57 | * Ensures a string containing only underscores, asterisks, line breaks, and question marks is returned 58 | 59 | # Exercise C 60 | 61 | Now that you have test coverage, we are nearly ready to refactor the core logic. Before we can fix anything, we 62 | need to isolate the code that is causing the problem. In order to do this, we need to break the `toString` 63 | into even smaller pieces. We will do two structural changes that will help with subsequent changes: 64 | 65 | * Create a new interface called `Board` with two properties: `x: number`, `y: number` 66 | * Refactor the class to expect a `size: Board`, `guesses`, and `mineCount` as constructor arguments. This will break 67 | previous tests. Fix those tests. You will also need to update `printMineSweeperBoard()` to provide the legacy default 68 | values to `toString` (i.e., the value of `x` and `y` need to be set to 10. 69 | 70 | If you haven't encounter *the* bug yet -- great. Let's encounter it now. Try updating your previous test that tests the 71 | mine placement to attempt to place 99 mines in a 10x10 grid (all squares except 1 will be filled with a mine). The test 72 | should fail nearly every time. 73 | 74 | Now we need to update the actual mine placement logic. We will simulate a situation where core logic is being changed: 75 | 76 | * Inside the inner `for` loop, there is an `if` block that contains logic for determining if a mine should be placed. 77 | Extract *the if condition* into a method with the signature: `shouldPlaceMine: () => bool`. 78 | * Add a `getMineLocations: () => Array` method (that will be used in the next step) that predetermines 79 | which positions mines should be placed in. The method returns an array of positions that mines should be placed at and if 80 | it runs again, returns the same results. 81 | * Fix the logic for `shouldPlaceMine`: right now it gives early squares a higher chance of getting a mine (this is the 82 | bug) and possibly does not place enough mines. Use the results from `getMineLocations` to make sure all mines always get placed. DO NOT SPEND TOO MUCH TIME TRYING TO GET PERFECT DISTRIBUTION -- the important part is that all required mines are always placed. 83 | 84 | The above illustrates an example of how one might start to refactor a block of procedural code. There's much more refactoring 85 | that could be done, which is left as optional exercises: 86 | 87 | * Lower the number of mines being placed and update your tests for `printMineSweeperBoard`: stub `getMineLocations()` 88 | so that it returns a predetermined set of coordinates that you can write your tests against. The randomly broken tests should now always pass. Make sure to check if `getMineLocations` is getting called (via spies). 89 | * Add a method for printing a mine: change the line with the ternary operator to call a method instead: 90 | `placeMine: (coordinate: MineCoordinate) => string` which will return either `*` or `?` based on if the location has been 91 | guessed during the constructor. 92 | * Extract the logic to check if a guess overlaps with a mine placement (the line that mentions `guesses[k].x == j`) 93 | * Add a border to the entire board 94 | * Extract a method to build a row (instead of an additional inner `for` loop) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview [![Build Status](https://travis-ci.org/michikono/typescript-tdd-exercises.svg?branch=master)](https://travis-ci.org/michikono/typescript-tdd-exercises) 2 | 3 | This project contains a set of exercises to help teach TDD. It will auto-compile your TypeScript code, run your tests 4 | and, enforce a 5 | minimum code coverage percentage (95%). 6 | 7 | # Installing and running 8 | 9 | Clone this project. You'll need to install [npm](https://docs.npmjs.com/getting-started/installing-node) and change your current working directory to this folder. 10 | 11 | Install all dependencies: 12 | 13 | ```shell 14 | $ npm install 15 | ``` 16 | 17 | DO NOT use `sudo` in this step or you will have problems! 18 | 19 | ## Setting up TypeScript watchers (compiles your code) 20 | 21 | For these exercises, we will use [Grunt](http://gruntjs.com/) to manage the build process. 22 | 23 | Test that `npm watch` is working correctly: 24 | 25 | ```shell 26 | $ npm run watch 27 | ``` 28 | 29 | You should see it "waiting..." If so, CTRL + C to break out and then run the tests manually: 30 | 31 | ```shell 32 | $ npm test 33 | ``` 34 | 35 | If you see the following, your setup process is complete: 36 | 37 | ![expected output](./assets/results.png) 38 | 39 | To get notifications working, you may need to install the following: 40 | 41 | 1. Install [Growl](http://growl.info/downloads#growlnotify) 42 | 2. Run `sudo gem install terminal-notifier` 43 | 44 | ## Seeing Code Coverage 45 | 46 | You can see code coverage analysis in two ways: 47 | 48 | 1. Make a change while `grunt watch` is on and read the console 49 | 2. Run `grunt test` and read the console 50 | 51 | Then visit [the lcov report page](./coverage/reports/lcov-report/index.html) (file will be missing if a coverage 52 | analysis has not been run) 53 | 54 | # Under the Hood 55 | 56 | You can find all of the grunt configurations in the `grunt` folder. Here's what Grunt is doing: 57 | 58 | 1. Compiled TypeScript output (JavaScript) goes to the [`out`](./out) folder while preserving the original file/folder 59 | structure. 60 | 2. All of the `.js` files are then copied (concatenated) to [`out/test.js`](./out/test.js) and 61 | [`out/coverage.js`](./out/coverage.js) file. The source maps are preserved. 62 | 3. All `out/src/**/*.js` files are "instrumented" by Istanbul for code coverage analysis. The results are stored in 63 | [`out/instrument`](./out/instrument). 64 | 4. Code coverage involves running all tests against this instrumented folder. The instrumented files are merged with 65 | test files and placed in [`out/coverage.js`](./out/coverage.js). The file is executed. 66 | 5. Coverage results are captured in [`coverage/`](./coverage/). 67 | 6. Coverage thresholds are configured in [`grunt/coverage.js`](./grunt/coverage.js) and the data is pulled from 68 | [`coverage/reports/coverage.json`](./coverage/reports/coverage.json). 69 | 7. If any grunt process fails (e.g., a test or a coverage threshold) an error is shown and the rest of the jobs stop. 70 | 8. `npm run watch` watches for code changes that trigger the above steps automatically as necessary. It is meant to be 71 | noisy if you are not writing passing tests. 72 | 73 | # Exercises 74 | 75 | 1. [Exercise 1](./src/exercise1/README.md) 76 | 2. [Exercise 2](./src/exercise2/README.md) 77 | 3. [Exercise 3](./src/exercise3/README.md) 78 | 4. [Exercise 4](./src/exercise4/README.md) 79 | 80 | Review the [example project](./src/example/README.md) if you have questions about TypeScript. 81 | 82 | # Notes 83 | 84 | ## TypeScript 85 | 86 | * Pay special attention to nesting modules: top level modules don't use `export`, but children should. See more here: 87 | [http://stackoverflow.com/questions/12991382/typescript-modules](http://stackoverflow.com/questions/12991382/typescript-modules) 88 | * TypeScript files should always start with a reference line (think of it as a header file) that links to 89 | [references.ts](./references.ts) using a relative path; for example: 90 | 91 | ```typescript 92 | /// 93 | ``` 94 | 95 | ## Structure 96 | 97 | * Executed JS is not kept as separate files. This is to avoid the complexity of using Node's `require` syntax in a 98 | TypeScript environment (although entirely doable). 99 | * You can find example files at [`src/example/`](./src/example/) and [`test/example/`](./test/example/). 100 | * You can find instructions for individual exercises in the [`src/`](./src/) folder. 101 | * TypeScript declarations are found in [`tsd.d.ts`](./tsd.d.ts), and are managed using `tsd`. They are like `.h` files 102 | in other languages. If you end up using external libraries such as Underscore.js, you may want to install dependencies 103 | using `tsd install [name] --save` (more on this here: 104 | [https://github.com/DefinitelyTyped/tsd](https://github.com/DefinitelyTyped/tsd)) 105 | * Folders you may want to mark as excluded from your IDE's code index: `out/`, `node_modules/`, and `coverage/`. 106 | 107 | ## Resources 108 | 109 | * This setup process borrows from the [Typescript Starter repo](https://github.com/michikono/typescript-starter) 110 | * You need to install `npm` (`node` [comes with it](http://nodejs.org/download/)). This is because TypeScript 111 | compiles to JavaScript and without Node, you would need to run your code in a browser. Running sample snippets in a 112 | browser adds unnecessary complexity as compared to running Node scripts. 113 | * Testing is done using two libraries. One is [Mocha](http://mochajs.org/#getting-started), a framework for writing 114 | assertions (the `assert` variable). The other is [Sinon](http://sinonjs.org/docs/), a stubbing and mocking library 115 | (the `sinon` and `sandbox` variables). See examples of this in [Driver tests](./test/example/driver.ts). 116 | * As a convenience, all test modules have a `sandbox` variable that you can use to make stubs. It will clean up your 117 | mocks and stubs after each test run. You can mostly ignore this, but if you are curious why this was setup, you can 118 | find more about it [here](http://sinonjs.org/docs/#sandbox). 119 | 120 | ## Troubleshooting 121 | 122 | If you are getting problems installing the npm packages, try fixing the directory permissions. Go to the working directory for the project: 123 | 124 | ```shell 125 | sudo chown -R $USER ~/.npm # <== your user's npm cache folder 126 | sudo chown -R $USER /usr/local/lib/node_modules # <== your global npm folder 127 | sudo rm -Rf node_modules 128 | npm install 129 | ``` 130 | --------------------------------------------------------------------------------