diff --git a/README.md b/README.md index 66d2b43..93b55d1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ $ mkdir test Next in your favourite editor, create `test/test.mjs`: -```node +```javascript import { Eltro as t, assert} from 'eltro' t.describe('Array', function() { @@ -74,14 +74,14 @@ Not only does eltro allow you to use any assertion library of your own choosing, * `assert.notMatch(value, [message])`: Check if value does not match RegExp test. * `assert.isFulfilled(promise, [message])`: Assert the promise resolves. * `assert.isRejected(promise, [message])`: Assert the promise gets rejects. - - # Asynchronous Code + +# Asynchronous Code Eltro supports any type of asynchronous code testing. It can either be done by adding a parameter to the function (usually done) that gets called once the tests done but eltro also supports promises. Example of testing using done: -```node +```javascript import { Eltro as t, assert} from 'eltro' t.describe('User', function() { @@ -99,7 +99,7 @@ t.describe('User', function() { Alternatively, just use the done() callback directly (which will handle an error argument, if it exists): -```node +```javascript import { Eltro as t, assert} from 'eltro' t.describe('User', function() { @@ -114,7 +114,7 @@ t.describe('User', function() { Or another alternative is to use promises and return a promise directly: -```node +```javascript import { Eltro as t, assert} from 'eltro' t.test('should complete this test', function(done) { @@ -126,13 +126,37 @@ t.test('should complete this test', function(done) { Which works well with `async/await` like so: -```node +```javascript t.test('async test', async function() { let user = await User.find({ username: 'test' }) assert.ok(user) }) ``` +# Spying and stubbing + +Inspired by sinon js, this library comes with pre-built simple sinon-like style spy() and stub() + +```javascript +import { assert, spy, stub } from 'eltro' + +let myFunc = spy() +let myStub = stub() + +myFunc(1) +myFunc(2) +myFunc(3) +myStub.returns('world') +let out = myStub('hello') + +assert.strictEqual(out, 'world') +assert.strictEqual(myFunc.getCall(0), 1) +assert.strictEqual(myFunc.getCall(1), 2) +assert.strictEqual(myFunc.getCall(2), 3) +assert.strictEqual(myFunc.callCount, 3) +assert.strictEqual(myStub.callCount, 1) +``` + # Api ### t.test(message, func) @@ -143,7 +167,7 @@ Queue up the `func` as a test with the specified messagt. In case you wanna describe a bunch of tests, you can add them inside `func` and it will have the specified `message` prepended before every test: -```node +```javascript import { Eltro as t, assert} from 'eltro' function someFunction() { return true } @@ -167,7 +191,7 @@ will output: Queue up the `func` to run before any test or groups within current active group. -```node +```javascript import { Eltro as t, assert} from 'eltro' t.before(function() { @@ -199,7 +223,7 @@ t.describe('#anotherTest()', function() { Queue up the `func` to run after any test or groups within current active group. -```node +```javascript import { Eltro as t, assert} from 'eltro' t.after(function() { @@ -227,13 +251,77 @@ t.describe('#anotherTest()', function() { }) ``` +### t.beforeEach(func) + +Queue up the `func` to run before each test or groups within current active group. + +```javascript +import { Eltro as t, assert} from 'eltro' + +t.beforeEach(function() { + // Prepare something before each of the following tests +}) + +t.describe('#myTest()', function() { + t.beforeEach(function() { + // Runs before every test in this group + }) + + t.test('true should always be true', function() { + assert.strictEqual(true, true) + }) +}) + +t.describe('#anotherTest()', function() { + t.beforeEach(function() { + // Runs before every test in this group + }) + + t.test('false should always be false', function() { + assert.strictEqual(false, false) + }) +}) +``` + +### t.afterEach(func) + +Queue up the `func` to run after every test or groups within current active group. + +```javascript +import { Eltro as t, assert} from 'eltro' + +t.afterEach(function() { + // After we finish each individual test below, this gets run +}) + +t.describe('#myTest()', function() { + t.afterEach(function() { + // Runs after each text in this group + }) + + t.test('true should always be true', function() { + assert.strictEqual(true, true) + }) +}) + +t.describe('#anotherTest()', function() { + t.afterEach(function() { + // Runs after each text in this group + }) + + t.test('false should always be false', function() { + assert.strictEqual(false, false) + }) +}) +``` + ### t.only() Eltro supports exclusivity when running tests. When specified, only tests marked with only will be run. You can do exclusivity on tests by adding `.only()` in front of describe, after or before the test like so: -```node +```javascript t.only().describe('Only these will run', function() { t.test('this one', function() { assert.strictEqual(true, true) }) t.test('and this one', function() { assert.strictEqual(true, true) }) @@ -242,7 +330,7 @@ t.only().describe('Only these will run', function() { You can also put it on individual test like so -```node +```javascript t.test('Only run this test', function() { assert.strictEqual(true, true) }).only() @@ -250,7 +338,7 @@ t.test('Only run this test', function() { or like so: -```node +```javascript t.only().test('Only run this test', function() { assert.strictEqual(true, true) }) @@ -260,7 +348,7 @@ t.only().test('Only run this test', function() { You can skip tests easily by adding `.skip()` before describe, before or after the test like so: -```node +```javascript t.skip().describe('None of these will run', function() { t.test('not this', function() { assert.strictEqual(true, true) }) t.test('or this one', function() { assert.strictEqual(true, true) }) @@ -269,7 +357,7 @@ t.skip().describe('None of these will run', function() { You can also do it on individual tests like so: -```node +```javascript t.test('Skip due to something being broken', function() { BrokenFunction() }).skip() @@ -277,7 +365,7 @@ t.test('Skip due to something being broken', function() { or like so: -```node +```javascript t.skip().test('Skip this', function() { ... }) ``` @@ -285,7 +373,7 @@ t.skip().test('Skip this', function() { ... }) Tests can take a long time. By default, eltro will cancel a test if it takes longer than 2 seconds. You can however override this by calling the timeout function after or before the test or before the describe with the specified duration in milliseconds like so: -```node +```javascript t.timeout(5000).describe('These will all have same timeout', function() { t.test('One slow function', async function() { ... }) t.test('Another slow function', async function() { ... }) @@ -294,7 +382,7 @@ t.timeout(5000).describe('These will all have same timeout', function() { Or apply to individual test like so: -```node +```javascript t.test('This is a really long test', async function() { await DoSomethingForReallyLongTime() }).timeout(5000) // 5 seconds @@ -302,6 +390,277 @@ t.test('This is a really long test', async function() { or like so: -```node +```javascript t.timeout(5000).test('A long test', async function() { ... }) ``` + +# Assert + +Eltro comes with an extended version of node's built-in assertion library. +You can start using them by simply importing it with eltro test runner: + +```javascript +import { assert } from 'eltro' +``` + + +### assert.notOk(value[, message]) + +Tests if value is a falsy value using `Boolean(value) == false` + +```javascript +assert.notOk(false) // ok +assert.notOk(null) // ok +assert.notOk(undefined) // ok +assert.notOk([]) // throws +``` + +### assert.match(value, test[, message]) + +Test if the string value has a regex match of test + +```javascript +assert.match('asdf', /a/) // ok +assert.match('hello world', /hello/) // ok +assert.match('something', /else/) // throws +``` + +### assert.notMatch(value, test[, message]) + +Test if the string value does not regex match the test + +```javascript +assert.notMatch('asdf', /b/) // ok +assert.notMatch('something', /else/) // ok +assert.notMatch('hello world', /hello/) // throws +``` + +### assert.isFulfilled(promise[, message]) + +Tests to make sure the promise gets fulfilled successfully and +returns the final result. + +```javascript +await assert.isFulfilled(Promise.resolve(null)) // ok +await assert.isFulfilled(() => { throw new Error() }) // throws +``` + +### assert.isRejected(promise[, message]) + +Tests to make sure the promise gets rejected and returns the error +or value that was rejected + +```javascript +let val = await assert.isRejected(Promise.reject('asdf')) // ok +assert.strictEqual(val, 'asdf') + +let err = await assert.isRejected(() => { throw new Error('hello') }) // ok +assert.strictEqual(err.message, 'hello') +``` + +### assert.throwsAndCatch(fn[, message]) + +Tests to make sure the function throws an exception. The important feature is this returns the original error that was thrown. + +```javascript +let err = assert.throwsAndCatch(() => { throw new Error('hello world') }) // ok +assert.strictEqual(err.message, 'hello world') +``` + +# Sinon-like spy() stub() + +Using sinon-inspired mechanics for spying on calls as well as being able +to stub existing functionality, eltro comes with a handy little copy-cat. + +Functionality-wise, the difference between spy() and stub() are none. +Both will do the exact same thing, the naming differention is simply to allow +the resulting code to speak about its purpose. + +To create a stub or a spy, simply import it and call it like so: + +```javascript +import { spy, stub } from 'eltro' + +let spying = spy() +let fn = stub() +``` + +Each call to stub or spy is an array list of the passed-on arguments: + +```javascript +let spying = spy() +spying('hello', 'world') + +assert.strictEqual(spying.lastCall[0], 'hello') +assert.strictEqual(spying.lastCall[1], 'world') +``` + +### lastCall + +Returns the last call that was made to the spy or stub: + +```javascript +let spying = spy() +spying('a') +spying('b') +spying('c') + +assert.strictEqual(spying.lastCall[0], 'c') +``` + +### called + +Boolean variable that gets flipped once it gets called at least once + +```javascript +let spying = spy() +assert.strictEqual(spying.called, false) +spying('a') +assert.strictEqual(spying.called, true) +spying('b') +spying('c') +assert.strictEqual(spying.called, true) +``` + +### callCount + +The number of times it's been called + +```javascript +let spying = spy() +assert.strictEqual(spying.callCount, 0) +spying('a') +assert.strictEqual(spying.callCount, 1) +spying('b') +spying('c') +assert.strictEqual(spying.callCount, 3) +``` + +### returns(data) + +Specifies what value the stub or spy should return when it gets called. + +```javascript +let fn = stub() +fn.returns('a') + +assert.strictEqual(fn(), 'a') +assert.strictEqual(fn(), 'a') +``` + +### throws(data) + +Specifies what value the stub or spy should throw when it gets called. + +```javascript +let fn = stub() +fn.throws(new Error('b')) + +try { + fn() +} catch (err) { + assert.strictEqual(err.message, 'b') +} +``` + +### resolves(data) + +Specifies what value the stub or spy should return wrapped in a promise. + +```javascript +let fn = stub() +fn.resolves('a') + +fn().then(function(data) { + assert.strictEqual(data, 'a') +}) +``` + +### rejects(data) + +Specifies what value the stub or spy should reject, wrapped in a promise. + +```javascript +let fn = stub() +fn.rejects('nope') + +fn().catch(function(data) { + assert.strictEqual(data, 'nope') +}) +``` + +### returnWith(fn) + +Specify custom function to be called whenever the stub or spy gets called. + +```javascript +let fn = stub() +fn.returnWith(function(a) { + if (a === 'a') return true + return false +}) + +assert.strictEqual(fn(), false) +assert.strictEqual(fn('b'), false) +assert.strictEqual(fn('a'), true) +assert.strictEqual(fn.callCount, 3) +``` + +### getCall(index) getCallN(num) + +Get a specific call. The `getCall` gets a zero-based index call while the `getCallN(num)` gets the more natural number call + +```javascript +let spying = spy() +spying('a') +spying('b') +spying('c') +assert.strictEqual(spying.getCall(0), 'a') +assert.strictEqual(spying.getCall(1), 'b') +assert.strictEqual(spying.getCallN(1), 'a') +assert.strictEqual(spying.getCallN(2), 'b') +``` + +### onCall(index) onCallN(num) + +Overwrite behavior for a specific numbered call. Just like with getCall/getCallN, the onCall is zero-indexed number of the call you want to specify custom behavior while onCallN is the more natural number of the call you want to specify custom behavior. + +Note, when called with null, it specifies the default behavior. + +```javascript +let fnOne = stub() +let fnTwo = stub() + +fnOne.onCall(1).returns('b') + .onCall().returns('a') + +fnTwo.onCallN(2).returns('two') + .onCallN().returns('one') + +assert.strictEqual(fnOne(), 'a') +assert.strictEqual(fnOne(), 'b') +assert.strictEqual(fnOne(), 'a') + +assert.strictEqual(fnTwo(), 'one') +assert.strictEqual(fnTwo(), 'two') +assert.strictEqual(fnTwo(), 'one') +``` + +### findCall(fn) + +Search for the first call when `fn(call)` returns `true`. Essentially a filter to search for a specific call that matches whatever call you're searching for. + + +```javascript +let evnt = stub() + +evnt('onclick', 'one') +evnt('onerror', 'two') +evnt('something', 'three') +evnt('onpress', 'four') +evnt('else', 'five') + +let foundPressCall = evnt.findCall(function(call) { return call[0] === 'onpress' }) +assert.strictEqual(foundPressCall[0], 'onpress') +assert.strictEqual(foundPressCall[1], 'four') +``` diff --git a/lib/assert.mjs b/lib/assert.mjs index 0d7cc06..e4fa038 100644 --- a/lib/assert.mjs +++ b/lib/assert.mjs @@ -48,6 +48,15 @@ assert.notMatch = (value, test, message) => { fail(m); } +assert.throwsAndCatch = (fn, message) => { + let err = null + assert.throws(fn, function(error) { + err = error + return true + }, message) + return err +} + assert.isFulfilled = (promise, message) => { return Promise.resolve(true) .then(() => promise) diff --git a/lib/sinon.mjs b/lib/sinon.mjs index 616fac8..0623163 100644 --- a/lib/sinon.mjs +++ b/lib/sinon.mjs @@ -35,6 +35,13 @@ export function stub(returnFunc = null) { func.called = false func.callCount = 0 + func.findCall = function(fn) { + for (let call of calls) { + if (fn(call)) return call + } + return null + } + func.getCall = function(i) { return calls[i] } diff --git a/package.json b/package.json index 6e0bc39..2aebaaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eltro", - "version": "1.3.2", + "version": "1.3.4", "description": "Eltro is a tiny no-dependancy test framework for node", "main": "index.mjs", "scripts": { diff --git a/test/assert.test.mjs b/test/assert.test.mjs index 6331300..69d16f2 100644 --- a/test/assert.test.mjs +++ b/test/assert.test.mjs @@ -29,6 +29,28 @@ t.describe('#notOk()', function() { }) }) +t.describe('#throwAndCatch()', function() { + t.test('should work and return the original error', function() { + const assertError = new Error('Speed') + + let err = assert.throwsAndCatch(() => { throw assertError }) + assert.strictEqual(err, assertError) + }) + + t.test('should otherwise throw if no error', function() { + const assertMessage = 'Hello world' + let error = null + + try { + assert.throwsAndCatch(() => { }, assertMessage) + } catch (err) { + error = err + } + assert.ok(error) + assert.match(error.message, new RegExp(assertMessage)) + }) +}) + t.describe('#isFulfilled()', function() { t.test('should exist', function() { assertExtended.ok(assertExtended.isFulfilled) diff --git a/test/sinon.test.mjs b/test/sinon.test.mjs index ead55e2..bfa6951 100644 --- a/test/sinon.test.mjs +++ b/test/sinon.test.mjs @@ -96,6 +96,35 @@ import { spy, stub } from '../index.mjs' assert.strictEqual(spyer.thirdCall[0], assertThirdArgs[0]) assert.strictEqual(spyer.thirdCall[1], assertThirdArgs[1]) }) + + + t.test('should support searching for a call', function() { + const assertFirstArgs = 'asdf' + const assertSecondArgs = { a: 1 } + const assertThirdArgs = [{ b: 1 }, { c: 2 }] + let spyer = tester() + assert.notOk(spyer.called) + + spyer(assertFirstArgs) + spyer(assertSecondArgs) + spyer(assertThirdArgs[0], assertThirdArgs[1]) + + let call = spyer.findCall((args) => args[0] === assertSecondArgs) + assert.strictEqual(spyer.secondCall, call) + assert.strictEqual(call[0], assertSecondArgs) + + call = spyer.findCall((args) => args[0].b === assertThirdArgs[0].b) + assert.strictEqual(spyer.thirdCall, call) + assert.strictEqual(call[0], assertThirdArgs[0]) + assert.strictEqual(call[1], assertThirdArgs[1]) + + call = spyer.findCall((args) => typeof args[0] === 'string') + assert.strictEqual(spyer.firstCall, call) + assert.strictEqual(call[0], assertFirstArgs) + + call = spyer.findCall(() => false) + assert.strictEqual(call, null) + }) }) })