├── .gitignore ├── README.md ├── exercises ├── call-me-maybe │ ├── index.js │ ├── instruction.md │ ├── solution.md │ └── tests │ │ ├── fail1.js │ │ ├── fail2.js │ │ └── pass.js ├── log-it-out │ ├── index.js │ ├── instruction.md │ ├── solution.md │ └── tests │ │ └── emotify.js ├── tape-it-together │ ├── index.js │ ├── instruction.md │ ├── solution.md │ └── tests │ │ ├── fail1.js │ │ ├── fail2.js │ │ ├── fail3.js │ │ └── pass.js ├── tell-me-what-is-wrong │ ├── index.js │ ├── instruction.md │ ├── solution.md │ └── tests │ │ ├── fail.js │ │ └── pass.js ├── to-err-is-human │ ├── index.js │ ├── instruction.md │ ├── solution.md │ └── tests │ │ ├── fail.js │ │ ├── fail2.js │ │ └── pass.js └── utils │ └── index.js ├── ideas.md ├── package.json └── test-anything.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _test 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This workshopper is deprecated. I recommend looking for a different resource to learn testing ☺️ 2 | 3 | # test-anything 4 | 5 | [![NPM](https://nodei.co/npm/test-anything.png)](https://nodei.co/npm/test-anything/) 6 | 7 | **Learn to test anything with TAP.** 8 | 9 | 10 | 1. Install [Node.js](http://nodejs.org/) 11 | 2. Run `sudo npm install test-anything -g` 12 | 3. Run `test-anything` 13 | 4. Select an exercise and go ahead :) 14 | -------------------------------------------------------------------------------- /exercises/call-me-maybe/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var verify = require('adventure-verify') 3 | var parse = require('../utils').parse 4 | var execTest = require('../utils').execTest 5 | var execRun = require('../utils').execRun 6 | 7 | exports.problem = parse(path.join(__dirname, 'instruction.md')) 8 | exports.solution = parse(path.join(__dirname, 'solution.md')) 9 | 10 | exports.verify = verify(execTest.bind( 11 | this, __dirname, ['fail1.js', 'fail2.js'], 'pass.js' 12 | )) 13 | 14 | exports.run = function (args) { 15 | execRun(args, __dirname) 16 | } 17 | -------------------------------------------------------------------------------- /exercises/call-me-maybe/instruction.md: -------------------------------------------------------------------------------- 1 | # Call me maybe 2 | 3 | Write a test for a function `repeatCallback(n, cb)`, that calls the callback 4 | `cb` exactly `n` times. `n` can be any number you want in your test code. 5 | 6 | As before the functions location will be provided through `process.argv[2]`. 7 | 8 | ## Hints 9 | 10 | Sometimes we are not simply checking return values of functions. A lot in 11 | JavaScript and node happens through callbacks and events. For this we often want 12 | to know: Was that callback called or not? 13 | 14 | The event-driven nature of JavaScript is also the reason why we had to call the 15 | `t.end()` function in the last level. The test has to know whether we are done. 16 | 17 | However there is maybe a better way to do this with callbacks using `t.plan(n)`. 18 | When we call this in the beginning we can tell `tape` how many assertions we are 19 | doing. 20 | 21 | ```js 22 | var test = require('tape') 23 | test('nextTick', function (t) { 24 | t.plan(1) 25 | process.nextTick(function () { 26 | t.pass('callback called') 27 | }) 28 | }) 29 | ``` 30 | 31 | In this example we only have one callback, which will simply pass the test when 32 | it is called. So we could have used `t.end()` within the callback instead. 33 | However you might see, that if we had multiple callbacks in our tests the 34 | `t.plan(n)` would come in handy. 35 | -------------------------------------------------------------------------------- /exercises/call-me-maybe/solution.md: -------------------------------------------------------------------------------- 1 | ```js 2 | var test = require('tape') 3 | var repeatCallback = require(process.argv[2]) 4 | 5 | test('repeatCallback', function (t) { 6 | t.plan(4) 7 | repeatCallback(4, function () { 8 | t.pass('callback called') 9 | }) 10 | }) 11 | ``` 12 | -------------------------------------------------------------------------------- /exercises/call-me-maybe/tests/fail1.js: -------------------------------------------------------------------------------- 1 | module.exports = function repeatCallback (n, cb) { 2 | if (n < 0) return 3 | cb() 4 | repeatCallback(n - 1, cb) 5 | } 6 | -------------------------------------------------------------------------------- /exercises/call-me-maybe/tests/fail2.js: -------------------------------------------------------------------------------- 1 | module.exports = function repeatCallback (n, cb) { 2 | if (n < 2) return 3 | cb() 4 | repeatCallback(n - 1, cb) 5 | } 6 | -------------------------------------------------------------------------------- /exercises/call-me-maybe/tests/pass.js: -------------------------------------------------------------------------------- 1 | module.exports = function repeatCallback (n, cb) { 2 | if (n < 1) return 3 | cb() 4 | repeatCallback(n - 1, cb) 5 | } 6 | -------------------------------------------------------------------------------- /exercises/log-it-out/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fork = require('child_process').fork 3 | var concat = require('concat-stream') 4 | var parse = require('../utils').parse 5 | var verify = require('adventure-verify') 6 | 7 | exports.problem = parse(path.join(__dirname, 'instruction.md')) 8 | exports.solution = parse(path.join(__dirname, 'solution.md')) 9 | 10 | exports.run = function (args) { 11 | run(args, 'running your module').pipe(process.stdout) 12 | } 13 | 14 | exports.verify = verify(function (args, t) { 15 | t.plan(1) 16 | 17 | var testString = Math.random() + 'test' 18 | 19 | run(args, testString).pipe(concat(function (result) { 20 | t.equal(result.toString(), testString + ' :)\n', 'adds smiley correctly') 21 | })) 22 | }) 23 | 24 | function run (args, string) { 25 | var opts = [path.join(__dirname, 'tests', 'emotify.js'), string] 26 | var program = fork(path.join(process.cwd(), args[0]), opts, {silent: true}) 27 | return program.stdout 28 | } 29 | -------------------------------------------------------------------------------- /exercises/log-it-out/instruction.md: -------------------------------------------------------------------------------- 1 | # Log it out 2 | 3 | Developing apps and modules is fun. However often you might be concerned whether 4 | things work or when you add new features you want to be sure you did not break 5 | anything. Therefore developers invented tests for their well-being. They allow 6 | you to automatically, well, test your application or module. 7 | 8 | Let's assume you wrote a function called `emotify`, which takes a String and 9 | adds a space and a `:)` to it. How would you check that your function is 10 | working? 11 | 12 | Maybe your first idea was calling the function with a value and `console.log` 13 | the result and then check its output in the console. 14 | 15 | ```js 16 | var emotify = require('./emotify.js') 17 | console.log(emotify('just testing')) 18 | ``` 19 | 20 | Try this yourself. We are going to provide the location for the awesome 21 | `emotify` module in `process.argv[2]` and the String for the test in 22 | `process.argv[3]`. 23 | -------------------------------------------------------------------------------- /exercises/log-it-out/solution.md: -------------------------------------------------------------------------------- 1 | ```js 2 | var emotify = require(process.argv[2]) 3 | console.log(emotify(process.argv[3])) 4 | ``` 5 | -------------------------------------------------------------------------------- /exercises/log-it-out/tests/emotify.js: -------------------------------------------------------------------------------- 1 | module.exports = function (word) { 2 | return word + ' :)' 3 | } 4 | -------------------------------------------------------------------------------- /exercises/tape-it-together/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var verify = require('adventure-verify') 3 | var parse = require('../utils').parse 4 | var execTest = require('../utils').execTest 5 | var execRun = require('../utils').execRun 6 | 7 | exports.problem = parse(path.join(__dirname, 'instruction.md')) 8 | exports.solution = parse(path.join(__dirname, 'solution.md')) 9 | 10 | exports.run = function (args) { 11 | execRun(args, __dirname) 12 | } 13 | 14 | exports.verify = verify(execTest.bind( 15 | this, __dirname, ['fail1.js', 'fail2.js', 'fail3.js'], 'pass.js' 16 | )) 17 | -------------------------------------------------------------------------------- /exercises/tape-it-together/instruction.md: -------------------------------------------------------------------------------- 1 | # Tape it together 2 | 3 | Write tests that output `TAP`, that tests the following properties of a function 4 | `fancify`. The function will be provided in `process.argv[2]`. 5 | 6 | 1 `fancify(str)` returns the `str` wrapped in `~*~` 7 | Example: `fancify('Hello')` returns `~*~Hello~*~` 8 | 2 It takes an optional second argument that converts the string into ALLCAPS 9 | Example: `fancify('Hello', true)` returns `~*~HELLO~*~` 10 | 3 It takes a third optional argument that determines the character in the middle 11 | Example: `fancify('Hello', false, '!')` returns `~!~Hello~!~` 12 | 13 | ## Hints 14 | 15 | Testing with `assert` still has some downsides. Even though we don't have to 16 | check all the values ourself like in the first level, but now we only get not 17 | very readable errors when something is wrong. Otherwise our tests don't do 18 | anything. Maybe we still would like to see some information that everything is 19 | ok. 20 | 21 | 22 | There is a standard for outputting data from tests called `TAP`, the 23 | `Test Anything Protocol`. It is nicely readable for humans as well as for our 24 | robotic friends. 25 | 26 | One module for testing that outputs `TAP` is `tape` (another one is `tap`, duh). 27 | It takes a description of what you are testing and a callback function, with a 28 | parameter `t` that works quite similar to `assert`. You use it to write your 29 | assertions. However it also has a function `t.end()`, that you call when you are 30 | done with your assertions. 31 | 32 | The `tape` module is not included in Node, so you need to install them in your 33 | project folder (where you keep your exercise files) with `npm install tape`. 34 | 35 | Here is an example how to test the last function with `tape` 36 | 37 | ```js 38 | var test = require('tape') 39 | var isCoolNumber = require('./cool.js') 40 | 41 | test('isCoolNumber accepts only cool numbers', function (t) { 42 | t.ok(isCoolNumber(42), '42 should be cool') 43 | t.end() 44 | }) 45 | ``` 46 | 47 | ## Resources 48 | 49 | * TAP on Wikipedia http://en.wikipedia.org/wiki/Test_Anything_Protocol 50 | * The tape module https://www.npmjs.org/package/tape -------------------------------------------------------------------------------- /exercises/tape-it-together/solution.md: -------------------------------------------------------------------------------- 1 | ```js 2 | var test = require('tape') 3 | var fancify = require(process.argv[2]) 4 | 5 | test('fancify', function (t) { 6 | t.equal(fancify('Wat'), '~*~Wat~*~', 'Wraps a string in ~*~') 7 | t.equal(fancify('Wat', true), '~*~WAT~*~', 'Optionally makes it allcaps') 8 | t.equal(fancify('Wat', false, '%'), '~%~Wat~%~', 'Optionally allows to set the character') 9 | t.end() 10 | }) 11 | ``` 12 | -------------------------------------------------------------------------------- /exercises/tape-it-together/tests/fail1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (str, allcaps, char) { 2 | str = 'wat the hell' 3 | if (allcaps) str = str.toUpperCase() 4 | char = char || '*' 5 | return '~' + char + '~' + str + '~' + char + '~' 6 | } 7 | -------------------------------------------------------------------------------- /exercises/tape-it-together/tests/fail2.js: -------------------------------------------------------------------------------- 1 | module.exports = function (str, allcaps, char) { 2 | char = char || '*' 3 | return '~' + char + '~' + str + '~' + char + '~' 4 | } 5 | -------------------------------------------------------------------------------- /exercises/tape-it-together/tests/fail3.js: -------------------------------------------------------------------------------- 1 | module.exports = function (str, allcaps, char) { 2 | if (allcaps) str = str.toUpperCase() 3 | char = '*' 4 | return '~' + char + '~' + str + '~' + char + '~' 5 | } 6 | -------------------------------------------------------------------------------- /exercises/tape-it-together/tests/pass.js: -------------------------------------------------------------------------------- 1 | module.exports = function (str, allcaps, char) { 2 | if (allcaps) str = str.toUpperCase() 3 | char = char || '*' 4 | return '~' + char + '~' + str + '~' + char + '~' 5 | } 6 | -------------------------------------------------------------------------------- /exercises/tell-me-what-is-wrong/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var parse = require('../utils').parse 3 | var execTest = require('../utils').execTest 4 | var execRun = require('../utils').execRun 5 | var verify = require('adventure-verify') 6 | 7 | exports.problem = parse(path.join(__dirname, 'instruction.md')) 8 | exports.solution = parse(path.join(__dirname, 'solution.md')) 9 | exports.verify = verify(execTest.bind(this, __dirname, ['fail.js'], 'pass.js')) 10 | exports.run = function (args) { 11 | execRun(args, __dirname) 12 | } 13 | -------------------------------------------------------------------------------- /exercises/tell-me-what-is-wrong/instruction.md: -------------------------------------------------------------------------------- 1 | # Tell me what's wrong 2 | 3 | Write a passing assertion for the function `isCoolNumber`, that will assure that 4 | it returns `true` when passing `42` in it. 5 | 6 | The path of the module exporting the function will be provided through 7 | `process.argv[2]`. 8 | 9 | ----- 10 | 11 | ## Hints 12 | 13 | Well this was probably nothing new for you. But wait don't leave, we are going 14 | to learn about some better ways to do this. 15 | 16 | If you are wondering, what's wrong about `console.log`, then think about this: 17 | If your functions are going to be more complex it is going to be harder and 18 | harder to actually read the output. You have to know what output is expected for 19 | every input and for different functions. 20 | 21 | So it would be better if our tests only told us about whether something works or 22 | not. Surely we could probably test each output with `!==` and warn if something 23 | is wrong like this. 24 | 25 | ```js 26 | if(add(2,1) !== 3) throw new Error('add(2,1) should be 3') 27 | ``` 28 | 29 | Now we get an error every time something is wrong, with the message what's not 30 | working. However in node there is a nice built-in module for this called 31 | `assert`. 32 | 33 | ```js 34 | var assert = require('assert') 35 | assert(add(2,1) === 3,'add(2,1) should be 3') 36 | ``` 37 | 38 | Or as alternatively: 39 | ```js 40 | assert.deepEqual(add(2,1), 3, 'add(2,1) should be 3') 41 | ``` 42 | 43 | Here are some functions you can use with assert. For a full list, see the 44 | documentation. 45 | ```js 46 | assert.ok(value, message) // tests if value is truthy 47 | assert.equal(actual, expected, message) // == 48 | assert.notEqual(actual, expected, message) // != 49 | assert.deepEqual(actual, expected, message) // for comparing objects 50 | assert.notDeepEqual(actual, expected, message) 51 | assert.strictEqual(actual, expected, message) // === 52 | assert.notStrictEqual(actual, expected, message) // !== 53 | ``` 54 | 55 | 56 | ## Resources 57 | - Node documentation: http://nodejs.org/api/assert.html 58 | -------------------------------------------------------------------------------- /exercises/tell-me-what-is-wrong/solution.md: -------------------------------------------------------------------------------- 1 | ```js 2 | var isCoolNumber = require(process.argv[2]) 3 | var assert = require('assert') 4 | assert(isCoolNumber(42)) 5 | ``` -------------------------------------------------------------------------------- /exercises/tell-me-what-is-wrong/tests/fail.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { 2 | return false 3 | } 4 | -------------------------------------------------------------------------------- /exercises/tell-me-what-is-wrong/tests/pass.js: -------------------------------------------------------------------------------- 1 | module.exports = function (n) { 2 | return n === 42 3 | } 4 | -------------------------------------------------------------------------------- /exercises/to-err-is-human/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var verify = require('adventure-verify') 3 | var parse = require('../utils').parse 4 | var execTest = require('../utils').execTest 5 | var execRun = require('../utils').execRun 6 | 7 | exports.problem = parse(path.join(__dirname, 'instruction.md')) 8 | exports.solution = parse(path.join(__dirname, 'solution.md')) 9 | 10 | exports.verify = verify(execTest.bind( 11 | this, __dirname, ['fail.js', 'fail2.js'], 'pass.js' 12 | )) 13 | 14 | exports.run = function (args) { 15 | execRun(args, __dirname) 16 | } 17 | -------------------------------------------------------------------------------- /exercises/to-err-is-human/instruction.md: -------------------------------------------------------------------------------- 1 | # To err is human 2 | 3 | A function `feedCat` takes any kind of food as a String argument and returns 4 | `'yum'` for everything you feed them. However if you try to feed the cat 5 | `'chocolate'`, the function will throw an error. 6 | 7 | Write tests for `feedCat` to be sure kittens can be fed yummy food without 8 | being harmed. 9 | 10 | The function will be provided through `process.argv[2]`. 11 | 12 | ## Hints 13 | 14 | > To err is human, to purr feline. - Robert Byrne 15 | 16 | Chocolate is awesome and so are cats. However they do not make a wonderful 17 | combination. The Caffeine and Theobromine in the chocolate can harm cats as well 18 | as dogs. 19 | 20 | Feeding chocolate to cats would therefore be considered an error. One way in 21 | JavaScript to deal with errors is to `throw` them (even though in Node this is 22 | probably not the best way). 23 | 24 | If we want to deal with these errors, we can use `try` and `catch` like this: 25 | 26 | ```js 27 | try { 28 | petDog('bordercollie') 29 | } 30 | catch(err) { 31 | console.error('It seems like it doesn\'t like that.') 32 | } 33 | ``` 34 | 35 | When we test things, we often say that we want to make sure that there are no 36 | errors. Well, that is not entirely true. We certainly want error-free code. 37 | However if someone else tries to do something weird with our functions, it 38 | still might be good to see an error. So good that we might want to test this 39 | behavior, e.g. to make sure there is no chocolate fed to cats. 40 | 41 | So maybe we know that a dachshund does not like to be petted. Well we could test 42 | this behavior like this: 43 | 44 | ```js 45 | t.throws(function () { 46 | petDog('dachshund') 47 | }) 48 | ``` 49 | 50 | Now the test expects an error and throws an error if there is no error. Mind 51 | boggling, right? 52 | 53 | By the way, if you are familiar with functional javascript, you might already 54 | know that you could also write it in one line: 55 | ```js 56 | t.throws(petDog.bind(null, 'dachhund')) 57 | ``` 58 | -------------------------------------------------------------------------------- /exercises/to-err-is-human/solution.md: -------------------------------------------------------------------------------- 1 | ```js 2 | var test = require('tape') 3 | var feedCat = require(process.argv[2]) 4 | 5 | test('cat feeding', function (t) { 6 | t.plan(2) 7 | t.equal(feedCat('food'), 'yum') 8 | t.throws(feedCat.bind(null, 'chocolate')) 9 | }) 10 | ``` -------------------------------------------------------------------------------- /exercises/to-err-is-human/tests/fail.js: -------------------------------------------------------------------------------- 1 | module.exports = function (food) { 2 | throw new Error('Do not feed cats!') 3 | } 4 | -------------------------------------------------------------------------------- /exercises/to-err-is-human/tests/fail2.js: -------------------------------------------------------------------------------- 1 | module.exports = function (food) { 2 | return 'yum' 3 | } 4 | -------------------------------------------------------------------------------- /exercises/to-err-is-human/tests/pass.js: -------------------------------------------------------------------------------- 1 | module.exports = function (food) { 2 | if (food === 'chocolate') { 3 | throw new Error('No, chocolate is dangerous!') 4 | } else { 5 | return 'yum' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /exercises/utils/index.js: -------------------------------------------------------------------------------- 1 | var md = require('cli-md') 2 | var fs = require('fs') 3 | var fork = require('child_process').fork 4 | var path = require('path') 5 | 6 | exports.parse = function (fileName) { 7 | return md(fs.readFileSync(fileName).toString()) 8 | } 9 | 10 | exports.execTest = function (dir, failFiles, passFile, args, t) { 11 | t.plan(failFiles.length + 1) 12 | 13 | var solutionFile = args[0] 14 | 15 | failFiles.forEach(function (testFile) { 16 | var program = fork(path.join(process.cwd(), solutionFile), 17 | [ path.join(dir, 'tests', testFile) ], 18 | {silent: true}) 19 | 20 | program.on('close', function (code) { 21 | t.ok(code, 'wrong function not accepted') 22 | }) 23 | }) 24 | 25 | var program = fork(path.join(process.cwd(), solutionFile), 26 | [ path.join(dir, 'tests', passFile) ], 27 | {silent: true}) 28 | 29 | program.on('close', function (code) { 30 | t.ok(!code, 'correct function accepted') 31 | }) 32 | } 33 | 34 | exports.execRun = function (args, dirname) { 35 | var out = '' 36 | out += [ 37 | 'Create your own module (`metatest.js`) to test your test.', 38 | 'Then run your code like this:', 39 | '`node ' + args[0] + ' ./metatest.js`', 40 | 'The `metatest.js` file could look like this to pass your', 41 | 'tests:' 42 | ].join('\n') 43 | 44 | out += '\n```js\n' + fs.readFileSync(path.join(dirname, 'tests/pass.js'), 'utf-8') + '\n```' 45 | 46 | out += '\nChange things in your `metatest.js` to make it fail your\ntests as well.' 47 | 48 | console.log(md(out)) 49 | } 50 | -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | - Teach about the exit code about the test in exercise 2 or 3 2 | - Other frameworks than tape (maybe optional bonus level) 3 | - travis-ci, appveyor (needs git-it knowledge) 4 | - pretty printer (faucet, etc.) 5 | - compare objects 6 | - subtests -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-anything", 3 | "version": "1.1.7", 4 | "description": "Learn to test anything with TAP", 5 | "main": "index.js", 6 | "bin": { 7 | "test-anything": "./test-anything.js" 8 | }, 9 | "scripts": { 10 | "test": "standard" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/finnp/test-anything.git" 15 | }, 16 | "keywords": [ 17 | "nodeschool", 18 | "workshopper", 19 | "learn", 20 | "cli", 21 | "tap" 22 | ], 23 | "author": "Finn Pauls", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/finnp/test-anything/issues" 27 | }, 28 | "homepage": "https://github.com/finnp/test-anything", 29 | "dependencies": { 30 | "adventure": "^2.11.0", 31 | "adventure-verify": "^2.2.0", 32 | "cli-md": "^1.0.0", 33 | "concat-stream": "^1.4.8", 34 | "tape": "^4.0.0", 35 | "workshopper-exercise": "^2.3.0" 36 | }, 37 | "preferGlobal": true, 38 | "devDependencies": { 39 | "standard": "^10.0.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test-anything.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var adventure = require('adventure') 4 | 5 | process.noDeprecation = true 6 | 7 | var shop = adventure({ 8 | title: 'TEST ANYTHING!', 9 | name: 'test-anything' 10 | }) 11 | 12 | shop.add('» LOG IT OUT', 13 | function () { return require('./exercises/log-it-out') }) 14 | shop.add('» TELL ME WHAT IS WRONG', 15 | function () { return require('./exercises/tell-me-what-is-wrong') }) 16 | shop.add('» TAPE IT TOGETHER', 17 | function () { return require('./exercises/tape-it-together') }) 18 | shop.add('» CALL ME MAYBE', 19 | function () { return require('./exercises/call-me-maybe') }) 20 | shop.add('» TO ERR IS HUMAN, TO PURR FELINE', 21 | function () { return require('./exercises/to-err-is-human') }) 22 | 23 | shop.execute(process.argv.slice(2)) 24 | --------------------------------------------------------------------------------