Skip to content
This repository has been archived by the owner on Jul 17, 2023. It is now read-only.

Commit

Permalink
test: epic improvement to test coverage 💪
Browse files Browse the repository at this point in the history
  • Loading branch information
robertrossmann committed Jul 10, 2018
1 parent 73126f5 commit aca95f3
Show file tree
Hide file tree
Showing 25 changed files with 211 additions and 46 deletions.
8 changes: 4 additions & 4 deletions packages/atlas/src/private/component-container.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class ComponentContainer {
* @param {Object} options.catalog Atlas catalog of all components
* @return {Promise<this>}
*/
async prepare(options = {}) {
async prepare(options) {
this.component.log.trace('prepare:before')

this::mkcatalog(this.#catalog, options)
Expand All @@ -138,11 +138,11 @@ class ComponentContainer {
/**
* Start the component
*
* @param {Object} opts={} Additional options
* @param {Object} opts Additional options
* @param {Map} opts.hooks Hooks available in the application
* @return {Promise<this.component>}
*/
async start(opts = {}) {
async start(opts) {
if (this.started) {
return this.component
}
Expand Down Expand Up @@ -224,7 +224,7 @@ class ComponentContainer {
*/
function mkobservers(observers, { hooks }) {
for (const [alias, container] of hooks) {
const target = (container.aliases || {})[container.Component.observes]
const target = container.aliases[container.Component.observes]

if (this.alias === target) {
observers.set(alias, container)
Expand Down
9 changes: 4 additions & 5 deletions packages/atlas/src/private/dispatch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ async function dispatch(event, subject) {
}
}

await Promise.all(Array.from(tasks.values()))

// Ensure no uncaught error escapes from this place
// This utilises the fact that we can catch any Promise-related errors by attaching a .catch block
// to the promise, even if the promise body has already executed. 💡
for (const [hook, task] of tasks) {
task.catch(err => void hook.component.log.error({ err, event }, 'hook:event:failure'))
}

// Ensure no uncaught error escapes from this place
await Promise.all(Array.from(tasks.values()))
.catch(() => {})
}

export default dispatch
26 changes: 26 additions & 0 deletions packages/atlas/test/aliasing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,30 @@ describe('Atlas: cross-component communication', () => {

expect(() => atlas.actions.dummy.ping('service:lolsvc')).to.throw(FrameworkError)
})

it('declaring a component of invalid type throws', () => {
class DummyAction extends Action {
static requires = ['invalid:name']
}

atlas.action('dummy', DummyAction, { aliases: { 'invalid:name': 'dummy' } })

return expect(atlas.start()).to.be.eventually.rejectedWith(
FrameworkError,
/Invalid component type: invalid used in alias invalid:name/,
)
})

it('resolving an alias into unknown component throws', () => {
class DummyAction extends Action {
static requires = ['service:dummy']
}

atlas.action('dummy', DummyAction, { aliases: { 'service:dummy': 'unknown' } })

return expect(atlas.start()).to.be.eventually.rejectedWith(
FrameworkError,
/Unable to find service unknown aliased as service:dummy/,
)
})
})
2 changes: 1 addition & 1 deletion packages/atlas/test/democonfig/serialisers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ const stdSerializers = {

export default {
...stdSerializers,
custom: () => {},
custom: true,
}
20 changes: 20 additions & 0 deletions packages/atlas/test/dispatch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,24 @@ describe('Hook: custom events', () => {
return expect(atlas.actions.dummy.trigger('handleOtherEvent'))
.to.not.eventually.be.rejectedWith(Error)
})

it('still resolves even if one of the hooks throws', async () => {
atlas = new Atlas({
root: __dirname,
config: {
atlas: { log: { level: 'fatal' } },
},
})
atlas.hook('dummy', DummyHook, { aliases: { 'action:dummy': 'dummy' } })
atlas.hook('another', AnotherHook, { aliases: { 'action:dummy': 'dummy' } })
atlas.action('dummy', DummyAction)

await atlas.start()

DummyHook.prototype.handleEvent = sinon.stub().resolves()
AnotherHook.prototype.handleEvent = sinon.stub().rejects(new Error('u-oh'))

return expect(atlas.actions.dummy.trigger('handleEvent'))
.to.not.be.eventually.rejectedWith(Error)
})
})
4 changes: 2 additions & 2 deletions packages/atlas/test/prepare.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DummyHook extends Hook {
}

class DummyAction extends Action {
dummyMethod() {}
dummyMethod = sinon.stub()
}

describe('Atlas::prepare()', () => {
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('Atlas::prepare()', () => {

atlas.hook('empty', Empty)

return expect(atlas.prepare()).to.be.rejectedWith(
return expect(atlas.prepare()).to.eventually.be.rejectedWith(
FrameworkError,
/does not have static 'observes' property/i,
)
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/commands/start.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class Start extends Command {
* @return {void}
*/
function forcequit() {
process.removeListener('SIGINT', forcequit)
process.removeListener('SIGTERM', forcequit)

throw new Error('Forced quit')
}

Expand Down
36 changes: 36 additions & 0 deletions packages/cli/test/commands/start.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,41 @@ describe('CLI: start', () => {

expect(disconnect).to.have.callCount(1)
})

it('throws an error to stop the process if a terminating signal is sent again', async () => {
await start.run()
start.doExit()

// Sending SIGINT causes the test suite to exit with code 130, so just test SIGTERM 🤷‍♂️
expect(() => process.emit('SIGTERM')).to.throw(Error, /Forced quit/)

delete process.exitCode
})

it('kills the process after 10s if an error occurs during stop procedure', async function() {
this.sandbox.stub(process, 'exit')
this.sandbox.stub(console, 'error')

const error = new Error('u-oh')
const clock = sinon.useFakeTimers({
toFake: ['setTimeout'],
})

start.atlas.stop.rejects(error)
await start.doExit()

// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(error.stack)
expect(process.exit).to.have.callCount(0)

clock.runAll()
clock.restore()

expect(process.exit).to.have.callCount(1)
expect(process.exitCode).to.equal(1)
expect(clock.now).to.equal(10000)

delete process.exitCode
})
})
})
4 changes: 2 additions & 2 deletions packages/component/test/api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Component: basics and API', () => {
})

it('saves the component function given on constructor to itself', () => {
const resolve = () => {}
const resolve = sinon.stub()
const component = new Component({
component: resolve,
})
Expand All @@ -53,7 +53,7 @@ describe('Component: basics and API', () => {
})

it('saves the dispatch function given on constructor to itself', () => {
const dispatch = () => {}
const dispatch = sinon.stub()
const component = new Component({ dispatch })

expect(component).itself.to.respondTo('dispatch')
Expand Down
23 changes: 23 additions & 0 deletions packages/errors/test/api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,27 @@ describe('Errors', () => {
expect(new errors.FrameworkError()).to.be.instanceOf(Error)
})
})

describe('ValidationError', () => {
it('exists', () => {
expect(errors.ValidationError).to.be.a('function')
})

it('inherits from FrameworkError', () => {
expect(new errors.ValidationError()).to.be.instanceOf(errors.FrameworkError)
})

it('exposes the constructor argument as this.errors', () => {
const data = { error: '123' }
const err = new errors.ValidationError(data)

expect(err.errors).to.eql(data)
})

it('works even if no errors are given to the constructor', () => {
const err = new errors.ValidationError()

expect(err.errors).to.be.an('object')
})
})
})
4 changes: 2 additions & 2 deletions packages/koa/src/middleware.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* Register the given middleware functions into the given Koa-compatible instance
*
* @param {Object} instance Koa-compatible instance. Must implement `.use()`.
* @param {Object} handlers={} Object where keys are the middlewares' names and the
* @param {Object} handlers Object where keys are the middlewares' names and the
* values are the actual middleware functions.
* @param {Object} config={} Configuration for individual middlewares. The keys should
* match the middleware names.
* @return {void}
*/
export default function middleware(instance, handlers = {}, config = {}) {
export default function middleware(instance, handlers, config = {}) {
for (const [name, handler] of Object.entries(handlers)) {
instance.use(handler(config[name]))
}
Expand Down
2 changes: 1 addition & 1 deletion packages/koa/test/context/api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Koa: ContextHook', () => {
})

it('throws when the property already exists on koa.context', () => {
server.context.testmethod = () => {}
server.context.testmethod = sinon.stub()
expect(() => hook.afterPrepare())
.to.throw(FrameworkError, /Unable to extend koa.context/)
})
Expand Down
5 changes: 3 additions & 2 deletions packages/koa/test/context/testcontext.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global sinon */
export default {
testmethod() {},
anothermethod() {},
testmethod: sinon.stub(),
anothermethod: sinon.stub(),
}
4 changes: 1 addition & 3 deletions packages/koa/test/server/stop.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ describe('Koa::stop(instance)', () => {
it('throws when called on an instance not yet prepared', () => {
delete instance.server
const msg = /Cannot stop a non-running server/
service = new Koa({
log: { info: () => {} },
})
service = new Koa({})
return expect(service.stop(instance)).to.eventually.be.rejectedWith(FrameworkError, msg)
})

Expand Down
12 changes: 12 additions & 0 deletions packages/koa/test/websocket/api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ describe('Hook: WebsocketHook', () => {
expect(args.first).to.eql({ firsttest: true })
expect(args.second).to.eql({ secondtest: true })
})

it('works without any middleware configured', async () => {
delete config.middleware

await hook.afterPrepare()
})
})


Expand Down Expand Up @@ -164,5 +170,11 @@ describe('Hook: WebsocketHook', () => {
await hook.beforeStop()
expect(koa.ws.server.close).to.have.callCount(1)
})

it('re-throws any errors returned from the koa.ws.server.close() method', () => {
koa.ws.server.close.callsArgWithAsync(0, new Error('u-oh'))

return expect(hook.beforeStop()).to.eventually.be.rejectedWith(Error, /u-oh/)
})
})
})
21 changes: 14 additions & 7 deletions packages/mongoose/test/service/prepare.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ describe('Mongoose::prepare()', () => {
beforeEach(async () => {
service = new Mongoose({
atlas: {},
log: {},
log: {
trace: sinon.stub(),
},
config: {},
})

Expand All @@ -24,13 +26,18 @@ describe('Mongoose::prepare()', () => {
expect(await service.prepare()).to.be.instanceof(mongoose.Mongoose)
})

it('sets a debug function to log model events', async function() {
this.sandbox.stub(Object.getPrototypeOf(instance), 'set')

it('sets a debug function to log model events', async () => {
await service.prepare()

expect(instance.set).to.have.callCount(1)
expect(instance.set).to.have.been.calledWith('debug')
expect(instance.set.firstCall.args[1]).to.be.a('function')
expect(instance.options.debug).to.be.a('function')

instance.options.debug('collection', 'method', 'arg1', 'arg2')

expect(service.log.trace).to.have.callCount(1)
expect(service.log.trace).to.have.been.calledWith({
collection: 'collection',
method: 'method',
args: ['arg1', 'arg2'],
})
})
})
3 changes: 2 additions & 1 deletion packages/objection/src/service.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class Objection extends Service {
},
}

prepare() {
// eslint-disable-next-line require-await
async prepare() {
const models = this.atlas.require(this.config.models) || {}
const connection = knex(this.config.knex)
const client = {
Expand Down
2 changes: 2 additions & 0 deletions packages/objection/test/service/models/index.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import ModelA from './model-a'
import ModelB from './model-b'
import ModelC from './model-c'

export {
ModelA,
ModelB,
ModelC,
}
5 changes: 5 additions & 0 deletions packages/objection/test/service/models/model-c.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Model } from '../../..'

export default class ModelC extends Model {
static tableName = 'modelc'
}
Loading

0 comments on commit aca95f3

Please sign in to comment.