├── 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 [](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 | 
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 |
--------------------------------------------------------------------------------