diff --git a/exercises/05_code_quality/currency/README.md b/exercises/05_code_quality/currency/README.md new file mode 100644 index 00000000..923f3332 --- /dev/null +++ b/exercises/05_code_quality/currency/README.md @@ -0,0 +1,4 @@ + +# Currency Converter + +This package converts from one currency to another using the current exchange rates. \ No newline at end of file diff --git a/exercises/05_code_quality/currency/index.js b/exercises/05_code_quality/currency/index.js new file mode 100644 index 00000000..b168850c --- /dev/null +++ b/exercises/05_code_quality/currency/index.js @@ -0,0 +1,53 @@ + +'use strict' + +const request = require('request') +const decimalPlaces = 2 + +exports.convert = (base, symbol, units, callback) => { + try { + checkValidCurrencyCode(base, err => { + if (err) callback(err) + checkValidCurrencyCode(symbol, err => { + if (err) callback(err) + base = base.trim() + symbol = symbol.trim() + getData(`http://api.fixer.io/latest?base=${base}&symbols=${symbol}`, (err, data) => { + if (err) callback(err) + const obj = JSON.parse(data) + //printObject(obj) + const result = parseFloat((obj.rates[symbol] * units).toFixed(decimalPlaces)) + //console.log(result) + callback(null, result) + }) + }) + }) + } catch(err) { + console.log(err.message) + } +} + +function checkValidCurrencyCode(code, callback) { + console.log(`currency: ${JSON.stringify(code)}`) + code = code.trim() + request('http://api.fixer.io/latest', (err, res, body) => { + if (err) callback(new Error('invalid API call')) + const rates = JSON.parse(body).rates + if (!rates.hasOwnProperty(code)) callback(new Error(`invalid currency code ${code}`)) + callback(null, true) + }) +} + +function getData(url, callback) { + request(url, (err, res, body) => { + if (err) callback(new Error('invalid API call')) + callback(null, body) + }) +} + +function printObject(data) { + const indent = 2 + const str = JSON.stringify(data, null, indent) + console.log(str) +} + diff --git a/exercises/05_code_quality/currency/package.json b/exercises/05_code_quality/currency/package.json new file mode 100644 index 00000000..328040ee --- /dev/null +++ b/exercises/05_code_quality/currency/package.json @@ -0,0 +1,12 @@ +{ + "name": "currency", + "version": "0.0.3", + "private": true, + "dependencies": { + "request": "^2.81.0" + }, + "devDependencies": { + "jasmine": "^2.5.3", + "jasmine-console-reporter": "^1.2.7" + } +} diff --git a/exercises/05_code_quality/currency/spec/currency-spec.js b/exercises/05_code_quality/currency/spec/currency-spec.js new file mode 100644 index 00000000..8ae73076 --- /dev/null +++ b/exercises/05_code_quality/currency/spec/currency-spec.js @@ -0,0 +1,36 @@ + +'use strict' + +// need to allow the use of arbitrary numbers when testing +/* eslint no-magic-numbers: 0 */ + +const currency = require('../index.js') + +describe('currency module', () => { + it('should return a number', done => { + currency.convert('GBP', 'USD', 100, (err, data) => { + try { + if(err) throw err // throw errors to the catch block + expect(data).toEqual(jasmine.any(Number)) + expect(data).toBeGreaterThan(0) + done() + } catch(err) { + expect(true).toBe(false) // no error should be thrown + } + }) + }) + + it('should return a value with 2 decimal places', done => { + currency.convert('GBP', 'USD', 111, (err, data) => { + try { + if(err) throw err // throw errors to the catch block + expect(data).toEqual(jasmine.any(Number)) + expect(data).toBeGreaterThan(0) + expect(data).toEqual(parseFloat(data.toFixed(2))) + done() + } catch(err) { + expect(true).toBe(false) // no error should be thrown + } + }) + }) +}) diff --git a/exercises/05_code_quality/currency/spec/jasmine.json b/exercises/05_code_quality/currency/spec/jasmine.json new file mode 100644 index 00000000..0ed867ba --- /dev/null +++ b/exercises/05_code_quality/currency/spec/jasmine.json @@ -0,0 +1,9 @@ + +{ + "spec_files": [ + "spec/currency-spec.js" + ], + "helpers": [ + "helpers/**/*.js" + ] +} \ No newline at end of file diff --git a/exercises/05_code_quality/currency/spec/runTests.js b/exercises/05_code_quality/currency/spec/runTests.js new file mode 100644 index 00000000..01aa6b56 --- /dev/null +++ b/exercises/05_code_quality/currency/spec/runTests.js @@ -0,0 +1,20 @@ + +'use strict' + +const Jasmine = require('jasmine') +const jasmine = new Jasmine() + +jasmine.loadConfigFile('spec/jasmine.json') + +const JasmineConsoleReporter = require('jasmine-console-reporter') +const reporter = new JasmineConsoleReporter({ + colors: true, + cleanStack: true, + verbosity: 4, + listStyle: 'indent', + activity: true +}) + +jasmine.addReporter(reporter) + +jasmine.execute() diff --git a/exercises/05_code_quality/currencyTest/index.js b/exercises/05_code_quality/currencyTest/index.js new file mode 100644 index 00000000..e00dd700 --- /dev/null +++ b/exercises/05_code_quality/currencyTest/index.js @@ -0,0 +1,37 @@ + +'use strict' + +const currency = require('currency') + +try { + getInput('enter base currency', (err, data) => { + if (err) throw err + const base = data + getInput('enter currency required', (err, data) => { + if (err) throw err + const symbol = data + getInput('enter amount to convert', (err, data) => { + if (err) throw err + const units = data + currency.convert(base, symbol, units, (err, data) => { + if (err) throw err + console.log(`${data} ${symbol}`) + }) + }) + }) + }) +} catch(err) { + console.log(err.message) +} + + +function getInput(prompt, callback) { + try { + process.stdin.resume() + process.stdin.setEncoding('utf8') + process.stdout.write(`${prompt}: `) + process.stdin.on('data', text => callback(null, text.trim())) + } catch(err) { + callback(err) + } +} diff --git a/exercises/05_code_quality/currencyTest/package.json b/exercises/05_code_quality/currencyTest/package.json new file mode 100644 index 00000000..f1385ba8 --- /dev/null +++ b/exercises/05_code_quality/currencyTest/package.json @@ -0,0 +1,14 @@ +{ + "name": "currency-test", + "version": "1.0.0", + "description": "test for the local currency package", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Mark J Tyers", + "license": "ISC", + "dependencies": { + "currency": "file:///Users/marktyers/Documents/304CEM/06 Writing Robust Code/currency" + } +} diff --git a/exercises/05_code_quality/shopping/debug.js b/exercises/05_code_quality/shopping/debug.js new file mode 100644 index 00000000..85c44586 --- /dev/null +++ b/exercises/05_code_quality/shopping/debug.js @@ -0,0 +1,25 @@ + +'use strict' + +const list = require('./modules/shopping') +debugger +list.add('bread') +list.add('butter') +debugger +list.add('cheese') +list.add('bread') +list.add('bread') +list.add('butter') +debugger +try { + list.decrement('bread', 2) + list.decrement('butter', 4) + list.decrement('jam', 3) +} catch(err) { + console.log('ERROR: '+err.message) +} + +const items = list.getAll() +items.forEach( (element, index) => { + console.log(index+'. '+element.title+'\t x'+element.qty) +}) \ No newline at end of file diff --git a/exercises/05_code_quality/shopping/index.js b/exercises/05_code_quality/shopping/index.js new file mode 100644 index 00000000..cabc0a0e --- /dev/null +++ b/exercises/05_code_quality/shopping/index.js @@ -0,0 +1,27 @@ +"use strict" + +const readline = require("readline-sync"); + +const list = require('./modules/shopping'); + +do + { + var input = String(readline.question("enter command: ")).trim() + if (input.indexOf('add ') == 0) { + let space = input.indexOf(' '); + let item = input.substring(space).trim(); + /* Let's echo the item details back to the user before adding it. Notice the use of Template Literals (https://goo.gl/Yjxa5o), a part of ECMA6. This allows you to include placeholders for variables without the need for concatenation. The string must be enclosed by backticks (`) */ + console.log(`adding "${item}"`) + list.add(item) + } + if (input.indexOf("list") == 0) { + const items = list.getAll() + let i = 1 + for (var value of items) { + /* process.stdout.write prints to the terminal without adding a newline character at the end. the \t character inserts a tab and the \n character inserts a newline */ + process.stdout.write(`${i}. ${value.title}`) + process.stdout.write(`\t${value.qty}\n`) + i++ + } + } +} while (input !== "exit"); diff --git a/exercises/05_code_quality/shopping/modules/shopping.js b/exercises/05_code_quality/shopping/modules/shopping.js new file mode 100644 index 00000000..5b6c0c69 --- /dev/null +++ b/exercises/05_code_quality/shopping/modules/shopping.js @@ -0,0 +1,93 @@ +'use strict' + +/** + * Shopping Module. + * @module shopping + */ + +/** this global 'map' object will be used to store the items and quantities. It stores each item against a named key. */ +var data = new Map(); + +/** add a new item to the todo list. Notice that we are using the new 'Arrow Function' syntax from the ECMA6 specification. */ +exports.add = item => { + /* check if the item is already in the list */ + if (data.get(item) == undefined) + { + /* if the item is not found it is added to the list with its quantity set to 1 */ + data.set(item, {title: item, qty: 1}) + } + else + { + /* the item is already in the list so it is retrieved and the quantity is incremented */ + let i = data.get(item) + i.qty++ + /* the new data is then stored in the map */ + data.set(item, i) + } + return data.get(item) +} + + +/** calculates and returns the number of items in the list */ +exports.count = () => { + /* the _Map_ object has a _size_ property that returns the number of items */ + return data.size +} + +/** empties the list */ +exports.clear = () => { + data = new Map() +} + +/** Returns an array containing all todo items */ +exports.getAll = () => { + let data_array = [] + for (var value of data.values()) { + data_array.push(value) + } + return data_array +} + +/** + * Returns details for the named item. + * @param {string} item - The item name to retrieve. + * @returns {string} The name of the item + * @throws {InvalidItem} item not in list. + */ +exports.getItem = item => { + if (data.get(item) === undefined) { + throw new Error('item not in list') + } + return data.get(item) +} + +/** Removes item with the corresponding name. */ +exports.removeItem = item => { + console.log('removeItem') + if (data.get(item) === undefined) { + throw new Error('item not in list') + } + data.delete(item) +} + +/** Decrements the quantity of an item in the list. */ +exports.decrement = (item, count) => { + console.log(arguments) + if (item === undefined || count === undefined) { + throw new Error('function requires two parameters') + } + if (data.get(item) === undefined) { + throw new Error('item not in list') + } + if (isNaN(count)) { + throw new Error('second parameter should be a number') + } + count = parseInt(count) + let current = data.get(item) + if (current.qty <= count) { + data.delete(item) + return + } + current.qty = current.qty - count + data.set(item, current) +} diff --git a/exercises/05_code_quality/shopping/package.json b/exercises/05_code_quality/shopping/package.json new file mode 100644 index 00000000..ecbfeb5d --- /dev/null +++ b/exercises/05_code_quality/shopping/package.json @@ -0,0 +1,30 @@ +{ + "name": "shopping", + "version": "1.0.0", + "description": "simple shopping list", + "main": "index.js", + "scripts": { + "app": "node index.js", + "lint": "node_modules/.bin/eslint modules/", + "test": "node_modules/.bin/jasmine-node spec --color --verbose --autotest --watch .", + "coverage": "./node_modules/.bin/istanbul cover -x **spec/** -x **index.js** -x **debug.js** ./node_modules/.bin/jasmine-node spec", + "doc": "node_modules/.bin/jsdoc modules/" + }, + "keywords": [ + "list", + "jasmine", + "module" + ], + "author": "Mark J Tyers", + "license": "ISC", + "dependencies": { + "readline-sync": "^1.2.22" + }, + "devDependencies": { + "eslint": "^3.1.0", + "istanbul": "^0.4.0", + "jasmine-node": "^1.14.5", + "jsdoc": "^3.4.0", + "node-inspector": "^0.12.8" + } +} diff --git a/exercises/05_code_quality/simpleDebug.js b/exercises/05_code_quality/simpleDebug.js new file mode 100644 index 00000000..a1116a19 --- /dev/null +++ b/exercises/05_code_quality/simpleDebug.js @@ -0,0 +1,25 @@ + +'use strict' + +const data = new Map() + +const readline = require('readline-sync') +let input +do { + input = String(readline.question('enter command: ')).trim() + debugger + if (input.indexOf('add ') === 0) { + const space = input.indexOf(' ') + const item = input.substring(space).trim() + console.log(`adding '${item}'`) + let qty = 1 + debugger + if (data.has(item)) qty = data.get(item) + data.set(item, qty) + } + if (input.indexOf('list') === 0) { + data.forEach( (val, key) => { + process.stdout.write(`${key}\t${val}\n`) + }) + } +} while (input !== 'exit') diff --git a/exercises/06_unit_testing/mongo/index.js b/exercises/06_unit_testing/mongo/index.js new file mode 100755 index 00000000..0bddd83d --- /dev/null +++ b/exercises/06_unit_testing/mongo/index.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +'use strict' + +const lists = require('./lists') + +const list = { + name: 'colours', + list: [ + 'red', + 'orange', + 'yellow' + ] +} + +lists.add(list, (err, data) => { + if (err) console.log(err) + console.log(data) + lists.count( (err, count) => { + if (err) console.log(err) + console.log(`${count} documents found`) + }) +}) diff --git a/exercises/06_unit_testing/mongo/jasmine.js b/exercises/06_unit_testing/mongo/jasmine.js new file mode 100755 index 00000000..b59c31c5 --- /dev/null +++ b/exercises/06_unit_testing/mongo/jasmine.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +'use strict' + +const Jasmine = require('jasmine') +const jasmine = new Jasmine() + +jasmine.loadConfigFile('jasmine.json') + +const JasmineConsoleReporter = require('jasmine-console-reporter') +const reporter = new JasmineConsoleReporter({ + colors: true, + cleanStack: true, + verbosity: 3, + listStyle: 'indent', + activity: true +}) + +jasmine.addReporter(reporter) + +jasmine.execute() diff --git a/exercises/06_unit_testing/mongo/jasmine.json b/exercises/06_unit_testing/mongo/jasmine.json new file mode 100644 index 00000000..bf13d563 --- /dev/null +++ b/exercises/06_unit_testing/mongo/jasmine.json @@ -0,0 +1,8 @@ +{ + "spec_files": [ + "lists-spec.js" + ], + "helpers": [ + "helpers/**/*.js" + ] +} diff --git a/exercises/06_unit_testing/mongo/lists-spec.js b/exercises/06_unit_testing/mongo/lists-spec.js new file mode 100755 index 00000000..e8a4c65d --- /dev/null +++ b/exercises/06_unit_testing/mongo/lists-spec.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +// node jasmine +// istanbul cover -x **spec/** -x **index.js** -x **debug.js** jasmine.js + +'use strict' + +// ./node_modules/.bin/jasmine-node --verbose lists-spec.js +// ./node_modules/.bin/istanbul cover ./node_modules/.bin/jasmine lists-spec.js + +// ./node_modules/.bin/istanbul cover -x **index.js** -x **lists-spec.js** -x **schema.js** ./node_modules/.bin/jasmine lists-spec.js + +const schema = require('./schema') +const lists = require('./lists') + +describe('shopping lists', () => { + beforeEach( done => { + schema.List.remove({}, err => { + if (err) expect(true).toBe(false) //error should not be thrown + new schema.List({name: 'colours', list: ['red', 'orange', 'yellow']}).save( (err, list) => { + if (err) expect(true).toBe(false) //error should not be thrown + schema.List.count({}, (err, count) => { + if (err) expect(true).toBe(false) //error should not be thrown + expect(count).toBe(1) + done() + }) + }) + }) + }) + + describe('add', () => { + it('should add a valid list', done => { + const shopping = { + name: 'shopping', + list: ['bread', 'butter', 'cheese'] + } + + lists.add(shopping, (err, data) => { + if (err) expect(true).toBe(false) + expect(data.name).toBe('shopping') + schema.List.count({}, (err, count) => { + if (err) expect(true).toBe(false) + expect(count).toBe(2) + done() + }) + }) + }) + }) + + describe('count', () => { + it('should find one list', done => { + lists.count( (err, count) => { + if (err) expect(true).toBe(false) + expect(count).toBe(1) + done() + }) + }) + }) + describe('remove', () => { + it('should remove an existing list', done => { + lists.remove('colours').then( () => { + schema.List.count({}, (err, count) => { + if (err) expect(true).toBe(false) + expect(count).toBe(0) + done() + }) + }).catch( err => { + if (err) expect(true).toBe(false) //error should not be thrown + }) + }) + }) + +}) diff --git a/exercises/06_unit_testing/mongo/lists.js b/exercises/06_unit_testing/mongo/lists.js new file mode 100755 index 00000000..85a4840d --- /dev/null +++ b/exercises/06_unit_testing/mongo/lists.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +'use strict' + +// this module demonstrates the use of callbacks and +// connecting to a MongoDB database, +// there is also an example of a synchronous function. + +const schema = require('./schema') + +exports.add = (data, callback) => { + if (this.validateData(data) === false) return callback(new Error('invalid data')) + const list = new schema.List(data) + + list.save( (err, list) => { + if (err) { + console.log(err) + callback(err) + } + callback(null, data) + }) +} + +// this exported function returns how many documents +// there are in the MongoDB database +exports.count = callback => { + schema.List.count({}, (err, count) => { + if (err) callback(err) + callback(null, count) + }) +} + + +// this is a standard anonymous function, +// returns true or throws error. +exports.validateData = data => { + if (!data.name || !data.list) return false + if (typeof data.name !== 'string' || !Array.isArray(data.list)) return false + if (data.list.length === 0) return false + return true +} + +// this is an anonymous function that returns a promise. +exports.remove = listName => new Promise( (resolve, reject) => { + schema.List.find({name: listName}).remove( err => { + if (err) return reject(err) + resolve() + }) +}) diff --git a/exercises/06_unit_testing/mongo/schema.js b/exercises/06_unit_testing/mongo/schema.js new file mode 100755 index 00000000..097bf9a4 --- /dev/null +++ b/exercises/06_unit_testing/mongo/schema.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +'use strict' + +// import the mongoose package +const mongoose = require('mongoose') +const db = { + user: 'testuser', + pass: 'password' +} + +mongoose.connect(`mongodb://${db.user}:${db.pass}@ds143777.mlab.com:43777/marktyers`) +mongoose.Promise = global.Promise +const Schema = mongoose.Schema + +// create a schema +const listSchema = new Schema({ + name: String, + list: [{type: String}] +}) + +// create a model using the schema +exports.List = mongoose.model('List', listSchema) diff --git a/exercises/06_unit_testing/shopping/debug.js b/exercises/06_unit_testing/shopping/debug.js new file mode 100755 index 00000000..05287374 --- /dev/null +++ b/exercises/06_unit_testing/shopping/debug.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +const list = require('./modules/shopping') +debugger +list.add('bread') +list.add('butter') +debugger +list.add('cheese') +list.add('bread') +list.add('bread') +list.add('butter') +debugger +try { + list.decrement('bread', 2) + list.decrement('butter', 4) + list.decrement('jam', 3) +} catch(err) { + console.log('ERROR: '+err.message) +} + +const items = list.getAll() +items.forEach( (element, index) => { + console.log(index+'. '+element.title+'\t x'+element.qty) +}) diff --git a/exercises/06_unit_testing/shopping/index.js b/exercises/06_unit_testing/shopping/index.js new file mode 100755 index 00000000..8efcbcdd --- /dev/null +++ b/exercises/06_unit_testing/shopping/index.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +'use strict' + +const readline = require('readline-sync') + +const list = require('./modules/shopping'); + +let input +do { + input = String(readline.question('enter command: ')).trim() + if (input.indexOf('add ') === 0) { + const space = input.indexOf(' ') + const item = input.substring(space).trim() + /* Let's echo the item details back to the user before adding it. Notice the use of Template Literals (https://goo.gl/Yjxa5o), a part of ECMA6. This allows you to include placeholders for variables without the need for concatenation. The string must be enclosed by backticks (`) */ + console.log(`adding "${item}"`) + list.add(item) + } + if (input.indexOf('list') === 0) { + const items = list.getAll() + let i = 1 + for (const value of items) { + /* process.stdout.write prints to the terminal without adding a newline character at the end. the \t character inserts a tab and the \n character inserts a newline */ + process.stdout.write(`${i}. ${value.title}`) + process.stdout.write(`\t${value.qty}\n`) + i++ + } + } +} while (input !== 'exit') diff --git a/exercises/06_unit_testing/shopping/modules/shopping.js b/exercises/06_unit_testing/shopping/modules/shopping.js new file mode 100755 index 00000000..ae7ab4c5 --- /dev/null +++ b/exercises/06_unit_testing/shopping/modules/shopping.js @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +'use strict' + +/** + * Shopping Module. + * @module shopping + */ + +/** this global 'map' object will be used to store the items and quantities. It stores each item against a named key. */ +const data = new Map() + +/** add a new item to the todo list. Notice that we are using the new 'Arrow Function' syntax from the ECMA6 specification. */ +exports.add = item => { + /* check if the item is already in the list */ + if (data.get(item) === undefined) { + /* if the item is not found it is added to the list with its quantity set to 1 */ + data.set(item, {title: item, qty: 1}) + } else { + /* the item is already in the list so it is retrieved and the quantity is incremented */ + const i = data.get(item) + i.qty++ + /* the new data is then stored in the map */ + data.set(item, i) + } + return data.get(item) +} + + +/** calculates and returns the number of items in the list */ +exports.count = () => data.size + +/** empties the list */ +exports.clear = () => { + data = new Map() +} + +/** Returns an array containing all todo items */ +exports.getAll = () => { + const dataArray = [] + for (const value of data.values()) { + dataArray.push(value) + } + return dataArray +} + +/** + * Returns details for the named item. + * @param {string} item - The item name to retrieve. + * @returns {string} The name of the item + * @throws {InvalidItem} item not in list. + */ +exports.getItem = item => { + if (data.get(item) === undefined) { + throw new Error('item not in list') + } + return data.get(item) +} + +/** Removes item with the corresponding name. */ +exports.removeItem = item => { + console.log('removeItem') + if (data.get(item) === undefined) { + throw new Error('item not in list') + } + data.delete(item) +} + +/** Decrements the quantity of an item in the list. */ +exports.decrement = (item, count) => { + console.log(arguments) + if (item === undefined || count === undefined) { + throw new Error('function requires two parameters') + } + if (data.get(item) === undefined) { + throw new Error('item not in list') + } + if (isNaN(count)) { + throw new Error('second parameter should be a number') + } + count = parseInt(count) + const current = data.get(item) + if (current.qty <= count) { + data.delete(item) + return + } + current.qty = current.qty - count + data.set(item, current) +} diff --git a/exercises/06_unit_testing/shopping/package.json b/exercises/06_unit_testing/shopping/package.json new file mode 100644 index 00000000..5d35a295 --- /dev/null +++ b/exercises/06_unit_testing/shopping/package.json @@ -0,0 +1,31 @@ +{ + "name": "shopping", + "version": "1.0.0", + "description": "simple shopping list", + "main": "index.js", + "scripts": { + "app": "node index.js", + "lint": "node_modules/.bin/eslint modules/", + "test": "node_modules/.bin/jasmine-node spec --color --verbose --autotest --watch .", + "coverage": "./node_modules/.bin/istanbul cover -x **spec/** -x **index.js** -x **debug.js** ./node_modules/.bin/jasmine-node spec", + "doc": "node_modules/.bin/jsdoc modules/" + }, + "keywords": [ + "list", + "jasmine", + "module" + ], + "author": "Mark J Tyers", + "license": "ISC", + "dependencies": { + "readline-sync": "^1.2.22" + }, + "devDependencies": { + "eslint": "^3.1.0", + "istanbul": "^0.4.0", + "jasmine": "^2.6.0", + "jasmine-console-reporter": "^1.2.7", + "jsdoc": "^3.4.0", + "node-inspector": "^0.12.8" + } +} diff --git a/exercises/06_unit_testing/shopping/spec/jasmine.json b/exercises/06_unit_testing/shopping/spec/jasmine.json new file mode 100644 index 00000000..f3fa9dc0 --- /dev/null +++ b/exercises/06_unit_testing/shopping/spec/jasmine.json @@ -0,0 +1,5 @@ +{ + "spec_files": [ + "spec/shopping-spec.js" + ] +} diff --git a/exercises/06_unit_testing/shopping/spec/runTests.js b/exercises/06_unit_testing/shopping/spec/runTests.js new file mode 100755 index 00000000..14be68e0 --- /dev/null +++ b/exercises/06_unit_testing/shopping/spec/runTests.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +'use strict' + +const Jasmine = require('jasmine') +const jasmine = new Jasmine() + +jasmine.loadConfigFile('spec/jasmine.json') + +const JasmineConsoleReporter = require('jasmine-console-reporter') +const reporter = new JasmineConsoleReporter({ + colors: true, + cleanStack: true, + verbosity: 4, + listStyle: 'indent', + activity: true +}) + +jasmine.addReporter(reporter) + +jasmine.execute() diff --git a/exercises/06_unit_testing/shopping/spec/shopping-spec.js b/exercises/06_unit_testing/shopping/spec/shopping-spec.js new file mode 100755 index 00000000..68b7570c --- /dev/null +++ b/exercises/06_unit_testing/shopping/spec/shopping-spec.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +'use strict' +/*istanbul ignore next*/ +/* eslint no-magic-numbers: 0 */ +/* global expect */ + +const list = require('../modules/shopping') + +describe('Shopping List', () => { + /* this code is run before each of the tests */ + beforeEach(() => { + list.add('bread') + list.add('bread') + list.add('butter') + }) + + /* this code is fun after EACH test */ + afterEach(() => { + list.clear() + }) + + it('should add a new item', () => { + list.add('cheese') + expect(list.count()).toBe(3) + }) + + it('should increment the qty if item exists', () => { + list.add('bread') + expect(list.count()).toBe(2) + }) + + it('should return an array containing all items in order of entry', () => { + const items = list.getAll() + expect(items[0].title).toBe('bread') + expect(items[0].qty).toBe(2) + expect(items[1].title).toBe('butter') + expect(items[1].qty).toBe(1) + }) + + /* by placing an 'x' in front of the function name we set its status to 'pending' which means the test won't run. In this way we can write lots of tests but focus on passing one test at a time. */ + it('should be able to return a shopping item', () => { + expect(list.getItem('bread').title).toBe('bread') + expect(list.getItem('bread').qty).toBe(2) + }) + + xit('should throw an error if item not in list', () => { + try { + list.getItem('jam') + expect(true).toBe(false) // we want to fail test if this line is run. + } catch(err) { + expect(err.message).toBe('item not in list') + } + }) + + xit('should delete first item', () => { + list.removeItem('bread') + expect(list.count()).toBe(1) + const items = list.getAll() + expect(items[0].title).toBe('butter') + }) + + xit('should delete last item', () => { + list.removeItem('butter') + expect(list.count()).toBe(1) + const items = list.getAll() + expect(items[0].title).toBe('bread') + }) + + xit('should throw error if item not in list', () => { + try { + list.removeItem('jam') + expect(true).toBe(false) + } catch(err) { + expect(err.message).toBe('item not in list') + } + }) + +}) diff --git a/exercises/06_unit_testing/taxi/index.js b/exercises/06_unit_testing/taxi/index.js new file mode 100755 index 00000000..28757ec1 --- /dev/null +++ b/exercises/06_unit_testing/taxi/index.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +'use strict' + +const taxi = require('./modules/taxi') + + +console.time('set ecb as home') +taxi.setHome('Gulson Road, Coventry', data => { + console.log('Setting ECB as home') + console.log(data) + console.log(data.lat) + console.timeEnd('set ecb as home') +}) + +console.time('standard fare') +taxi.getFare('University Road, Coventry', data => { + console.log(data) + console.timeEnd('standard fare') +}) + +console.time('set cathedral as home') +taxi.setHome('Coventry Cathedral', data => { + console.log('Coventry Cathedral') + console.log(data) + console.log(data.lat) + console.timeEnd('set cathedral as home') +}) + +console.time('long fare'); +taxi.getFare('Warwick Castle', data => { + console.log(data) + console.timeEnd('long fare') +}) + +console.time('short fare') +taxi.getFare('Broadgate, Coventry', data => { + console.log(data) + console.timeEnd('short fare') +}) diff --git a/exercises/06_unit_testing/taxi/modules/taxi.js b/exercises/06_unit_testing/taxi/modules/taxi.js new file mode 100644 index 00000000..a85ea8f3 --- /dev/null +++ b/exercises/06_unit_testing/taxi/modules/taxi.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +const sync = require('sync-request') + +let home + +exports.setHome = (location, callback) => { + //const loc = getLatLon(location) + home = getLatLon(location) + const latLng = home.split(',') + callback({lat: parseFloat(latLng[0]), lng: parseFloat(latLng[1])}) +} + +exports.getFare = function(destination, callback) { + const dest = getLatLon(destination) + const data = getRouteData(home, dest) // this is making the live API call + const distance = data.routes[0].legs[0].distance.value + const duration = data.routes[0].legs[0].duration.value + const cost = calculateFare(distance, duration) + callback({distance: distance, duration: duration, cost: parseFloat(cost)}) +} + +/* this function returns API data but the data won't vary between calls. We could get away without mocking this call. */ +function getLatLon(address) { + const url = `https://maps.googleapis.com/maps/api/geocode/json?region=uk&address=${address}` + console.log(url) + const res = sync('GET', url) + const data = JSON.parse(res.getBody().toString('utf8')) + const loc = data.results[0].geometry.location + return `${loc.lat.toFixed(6)}, ${loc.lng.toFixed(6)}` +} + +/* this function also returns live API data but this data will vary continously based on time of day and traffic conditions. This means it will require mocking in tests. by storing the function in a private variable we can substitute for a different function when testing. Because we are replacing the code block it will never be called by our test suite so we flag the code coverage tool to ignore it. */ +/* istanbul ignore next */ +const getRouteData = function(start, end) { + const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${start}&destination=${end}` + console.log(url) + const res = sync('GET', url) + return JSON.parse(res.getBody().toString('utf8')) +} + +function calculateFare(distance, duration) { + console.log(`distance: ${distance}km, duration: ${duration} min`) + const cost = distance/127*0.2 + return cost.toFixed(2) +} diff --git a/exercises/06_unit_testing/taxi/package.json b/exercises/06_unit_testing/taxi/package.json new file mode 100644 index 00000000..cde8641a --- /dev/null +++ b/exercises/06_unit_testing/taxi/package.json @@ -0,0 +1,25 @@ +{ + "name": "taxi", + "version": "1.0.0", + "description": "taxi fare calculator", + "main": "index.js", + "scripts": { + "test": "jasmine-node spec --color --verbose --noStack --autotest --watch .", + "coverage": "istanbul cover -x **/spec/** -x **index.js** --include-all-sources jasmine-node spec" + }, + "keywords": [ + "taxi", + "fare", + "testing" + ], + "author": "Mark J Tyers", + "license": "ISC", + "dependencies": { + "sync-request": "^2.1.0" + }, + "devDependencies": { + "jasmine-node": "^1.14.5", + "istanbul": "^0.4.0", + "rewire": "^2.3.4" + } +} diff --git a/exercises/06_unit_testing/taxi/spec/medium-spec.js b/exercises/06_unit_testing/taxi/spec/medium-spec.js new file mode 100755 index 00000000..7f310ffd --- /dev/null +++ b/exercises/06_unit_testing/taxi/spec/medium-spec.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +//const fs = require('fs') +const rewire = require('rewire') +const taxi = rewire('../modules/taxi') + +describe('Medium Length Route', () => { + + /* the routedata comes from an external API that is not guaranteed to return consistent data. We substitute a different function for testing that returns a fixed object. */ + /*taxi.__set__('getRouteData', function(start, end) { + console.log('MOCK 1') + const data = fs.readFileSync('spec/routedata/cov_war_uni.json', "utf8") + return JSON.parse(data) + })*/ + + it('should set Gulson Road, Coventry as the current location', done => { + taxi.setHome('Gulson Road, Coventry', data => { + expect(data.lat).toEqual(52.405899) + expect(data.lng).toEqual(-1.495929) + done() + }) + }) + + it('should calculate the fare to University Road, Coventry', done => { + taxi.getFare('University Road, Coventry', data => { + expect(data.distance).toEqual(7123) + expect(data.duration).toEqual(767) + expect(data.cost).toEqual(11.22) + done() + }) + }) + +}) diff --git a/exercises/06_unit_testing/taxi/spec/routedata/cov_uni_cat.json b/exercises/06_unit_testing/taxi/spec/routedata/cov_uni_cat.json new file mode 100644 index 00000000..bd0fae3e --- /dev/null +++ b/exercises/06_unit_testing/taxi/spec/routedata/cov_uni_cat.json @@ -0,0 +1,327 @@ +{ + "geocoded_waypoints": [ + { + "geocoder_status": "OK", + "place_id": "EjI4IENoYXJ0ZXJob3VzZSBSZCwgQ292ZW50cnksIFdlc3QgTWlkbGFuZHMgQ1YxLCBVSw", + "types": [ + "street_address" + ] + }, + { + "geocoder_status": "OK", + "place_id": "ChIJJwRfs7tLd0gR7AmZx2aFwmk", + "types": [ + "street_address" + ] + } + ], + "routes": [ + { + "bounds": { + "northeast": { + "lat": 52.4077306, + "lng": -1.4959289 + }, + "southwest": { + "lat": 52.40312549999999, + "lng": -1.5090762 + } + }, + "copyrights": "Map data ©2015 Google", + "legs": [ + { + "distance": { + "text": "1.7 km", + "value": 1700 + }, + "duration": { + "text": "5 mins", + "value": 291 + }, + "end_address": "24 Bayley Ln, Coventry, West Midlands CV1 5RJ, UK", + "end_location": { + "lat": 52.4077306, + "lng": -1.5071756 + }, + "start_address": "8 Charterhouse Rd, Coventry, West Midlands CV1, UK", + "start_location": { + "lat": 52.4058994, + "lng": -1.4959289 + }, + "steps": [ + { + "distance": { + "text": "0.4 km", + "value": 422 + }, + "duration": { + "text": "1 min", + "value": 80 + }, + "end_location": { + "lat": 52.4041836, + "lng": -1.5013684 + }, + "html_instructions": "Head west on Gulson Rd towards Vecqueray St", + "polyline": { + "points": "{oz~HpdcHRhBRfBPz@R~@VrALn@p@hDFRt@`Db@hBNl@@@BLDZJf@@@@BBB^V" + }, + "start_location": { + "lat": 52.4058994, + "lng": -1.4959289 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "0.1 km", + "value": 131 + }, + "duration": { + "text": "1 min", + "value": 22 + }, + "end_location": { + "lat": 52.4042584, + "lng": -1.502777 + }, + "html_instructions": "At the roundabout, take the 1st exit onto Short St", + "maneuver": "roundabout-left", + "polyline": { + "points": "cez~HpfdHLJPNHJFJDLDR@XAN?JAHCJCHCFMXIPSTGHEJEL" + }, + "start_location": { + "lat": 52.4041836, + "lng": -1.5013684 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "0.2 km", + "value": 158 + }, + "duration": { + "text": "1 min", + "value": 13 + }, + "end_location": { + "lat": 52.4044801, + "lng": -1.5050599 + }, + "html_instructions": "Take the slip road on the right to A45/City Centre/Ring Road/Birmingham", + "polyline": { + "points": "sez~HjodHCPCJAJAJ?JANAv@CfAC`BA~@AVCNKj@" + }, + "start_location": { + "lat": 52.4042584, + "lng": -1.502777 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "44 m", + "value": 44 + }, + "duration": { + "text": "1 min", + "value": 3 + }, + "end_location": { + "lat": 52.40447080000001, + "lng": -1.5057057 + }, + "html_instructions": "Merge onto Ringway St Johns/A4053", + "maneuver": "merge", + "polyline": { + "points": "_gz~Hr}dH?NAX@T?`@@^" + }, + "start_location": { + "lat": 52.4044801, + "lng": -1.5050599 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "0.2 km", + "value": 171 + }, + "duration": { + "text": "1 min", + "value": 16 + }, + "end_location": { + "lat": 52.4036568, + "lng": -1.5077488 + }, + "html_instructions": "At junction 5, take the New Union St exit to Barracks/Coventry University/Council House/Police Station/Law Courts/Tech Park/Service Areas", + "maneuver": "ramp-left", + "polyline": { + "points": "}fz~HtaeHJXDNDNDRJx@Hj@Hh@Jf@Lb@Rn@HVHJNNPH" + }, + "start_location": { + "lat": 52.40447080000001, + "lng": -1.5057057 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "0.2 km", + "value": 246 + }, + "duration": { + "text": "1 min", + "value": 37 + }, + "end_location": { + "lat": 52.4044562, + "lng": -1.5089504 + }, + "html_instructions": "At the roundabout, take the 4th exit onto New Union St", + "maneuver": "roundabout-left", + "polyline": { + "points": "{az~HlneHh@FTBFBHDDDDFBLBLBN?F@F?F?FAD?BCLCDADEFQLEDEDMFMFIDE@C@C?C?EAKAMAKCKAUCM?MBMDKFQLMN" + }, + "start_location": { + "lat": 52.4036568, + "lng": -1.5077488 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "0.2 km", + "value": 244 + }, + "duration": { + "text": "1 min", + "value": 44 + }, + "end_location": { + "lat": 52.4064844, + "lng": -1.5084757 + }, + "html_instructions": "Turn right onto Little Park St/B4544", + "maneuver": "turn-right", + "polyline": { + "points": "{fz~H|ueHOXMUYy@GGQMGA}@@_@?sBEe@AaAI" + }, + "start_location": { + "lat": 52.4044562, + "lng": -1.5089504 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "61 m", + "value": 61 + }, + "duration": { + "text": "1 min", + "value": 14 + }, + "end_location": { + "lat": 52.4069663, + "lng": -1.5081887 + }, + "html_instructions": "Slight left to stay on Little Park St/B4544", + "maneuver": "turn-slight-left", + "polyline": { + "points": "osz~H~reH_@@WAKKKMIOGO" + }, + "start_location": { + "lat": 52.4064844, + "lng": -1.5084757 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "58 m", + "value": 58 + }, + "duration": { + "text": "1 min", + "value": 11 + }, + "end_location": { + "lat": 52.406971, + "lng": -1.507342 + }, + "html_instructions": "Slight right onto Earl St/B4544", + "maneuver": "turn-slight-right", + "polyline": { + "points": "qvz~HdqeHCOAU?U@c@BiA" + }, + "start_location": { + "lat": 52.4069663, + "lng": -1.5081887 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "54 m", + "value": 54 + }, + "duration": { + "text": "1 min", + "value": 11 + }, + "end_location": { + "lat": 52.4069016, + "lng": -1.5065598 + }, + "html_instructions": "Keep left to stay on Earl St/B4544", + "maneuver": "keep-left", + "polyline": { + "points": "qvz~HzkeHDuAD}@@G" + }, + "start_location": { + "lat": 52.406971, + "lng": -1.507342 + }, + "travel_mode": "DRIVING" + }, + { + "distance": { + "text": "0.1 km", + "value": 111 + }, + "duration": { + "text": "1 min", + "value": 40 + }, + "end_location": { + "lat": 52.4077306, + "lng": -1.5071756 + }, + "html_instructions": "Turn left onto Bayley Ln