diff --git a/API.md b/API.md index e39e4aaa..fc145f74 100755 --- a/API.md +++ b/API.md @@ -393,7 +393,7 @@ Sets up a test where: - the function can return a Promise which either resolves (success) or rejects (fails). - all other return value is ignored. - `flags` - a set of test utilities described in [Flags](#flags). - + ```javascript lab.experiment('my plan', () => { @@ -418,17 +418,49 @@ The `test` function is passed a `flags` object that can be used to create notes #### `context` -An object that is passed to `before` and `after` functions in addition to tests themselves. `context` is used to set properties inside the before function that can be used by a test function later. It is meant to reduce module level variables that are set by the `before` / `beforeEach` functions. Tests aren't able to manipulate the context object for other tests. +An object that is passed to `before` and `after` functions in addition to tests themselves. `context` is used to set properties inside the before function that can be used by a test function later. It is meant to reduce module level variables that are set by the `before` / `beforeEach` functions. The context object is shallow cloned when passed to tests, as well as to child experiments, allowing you to modify it for each experiment individually without conflict through the use of `before`, `beforeEach`, `after` and `afterEach`. ```javascript -lab.before(({ context }) => { +lab.experiment('my experiment', () => { + + lab.before(({ context }) => { + + context.foo = 'bar'; + }) + + lab.test('contains context', ({ context }) => { + + expect(context.foo).to.equal('bar'); + }); - context.foo = 'bar'; -}) + lab.experiment('a nested experiment', () => { -lab.test('contains context', ({ context }) => { + lab.before(({ context }) => { - expect(context.foo).to.equal('bar'); + context.foo = 'baz'; + }); + + lab.test('has the correct context', ({ context }) => { + + expect(context.foo).to.equal('baz'); + // since this is a shallow clone, changes will not be carried to + // future tests or experiments + context.foo = 'fizzbuzz'; + }); + + lab.test('receives a clean context', ({ context }) => { + + expect(context.foo).to.equal('baz'); + }); + }); + + lab.experiment('another nested experiment', () => { + + lab.test('maintains the original context', ({ context }) => { + + expect(context.foo).to.equal('bar'); + }); + }); }); ``` @@ -748,7 +780,7 @@ Semantics: - `$lab:coverage:push$` copies the current skip state to the top of the stack, and leaves it as the current state as well - `$lab:coverage:pop$` replaces the current skip state with the top of the stack, and removes the top of the stack - if the stack is empty, `lab` will tell you by throwing the error `"unable to pop coverage bypass stack"` - + ### Excluding paths from coverage reporting The `--coverage-exclude` argument can be repeated multiple times in order to add multiple paths to exclude. By default the `node_modules` and `test` directories are excluded. If you want to exclude those as well as a directory named `public` you can run lab as follows: diff --git a/lib/runner.js b/lib/runner.js index 710157cb..d1581760 100755 --- a/lib/runner.js +++ b/lib/runner.js @@ -290,7 +290,7 @@ internals.executeExperiments = async function (experiments, state, skip, parentC !internals.experimentHasTests(experiment, state) || (state.options.bail && state.report.failures); - state.currentContext = parentContext ? Hoek.clone(parentContext) : {}; + state.currentContext = parentContext ? Hoek.clone(parentContext, { shallow: true }) : {}; // Before @@ -396,7 +396,7 @@ internals.executeTests = async function (experiment, state, skip) { const start = Date.now(); try { - test.context = Hoek.clone(state.currentContext); + test.context = Hoek.clone(state.currentContext, { shallow: true }); await internals.protect(test, state); } catch (ex) { diff --git a/test/runner.js b/test/runner.js index 22511b07..ba5386ac 100755 --- a/test/runner.js +++ b/test/runner.js @@ -1812,6 +1812,97 @@ describe('Runner', () => { expect(notebook.failures).to.equal(1); }); + it('passes shallow clones of context to tests', async () => { + + const script = Lab.script({ schedule: false }); + + const contextData = { + testData: { hello: 'there' }, + innerTestData: { goodbye: 'you' }, + additionalData: { another: 'object' }, + lateAddition: { more: 'data' } + }; + + script.experiment('test', () => { + + let outerContext; + + script.before(({ context }) => { + + outerContext = context; + context.testData = contextData.testData; + }); + + script.test('has test context', ({ context }) => { + + expect(context,'is deep equal').to.equal(outerContext); + expect(context,'is a shallow clone').to.not.shallow.equal(outerContext); + expect(context.testData,'is a reference').to.shallow.equal(contextData.testData); + context.additionalData = contextData.additionalData; + }); + + script.test('does not see changes to context from previous test', ({ context }) => { + + expect(context,'is deep equal').to.equal(outerContext); + expect(context,'is a shallow clone').to.not.shallow.equal(outerContext); + expect(context.testData,'is a reference').to.shallow.equal(contextData.testData); + expect(context.additionalData,'ignores mutation from another test').to.not.exist(); + }); + + script.experiment('child experiment', () => { + + let innerContext; + + script.before(({ context }) => { + + innerContext = context; + context.testData = contextData.innerTestData; + context.additionalData = contextData.additionalData; + }); + + script.afterEach(({ context }) => { + + context.lateAddition = contextData.lateAddition; + }); + + script.test('has the correct context', ({ context }) => { + + expect(innerContext,'is not the same reference').to.not.shallow.equal(outerContext); + expect(context,'is deep equal').to.equal(innerContext); + expect(context,'is a shallow clone').to.not.shallow.equal(innerContext); + expect(context.testData,'is a reference to inner data').to.shallow.equal(contextData.innerTestData); + expect(context.additionalData,'is a reference').to.shallow.equal(contextData.additionalData); + expect(outerContext.testData,'has not been changed').to.shallow.equal(contextData.testData); + }); + + script.test('receives context changes from afterEach', ({ context }) => { + + expect(innerContext,'is not the same reference').to.not.shallow.equal(outerContext); + expect(context,'is deep equal').to.equal(innerContext); + expect(context,'is a shallow clone').to.not.shallow.equal(innerContext); + expect(context.testData,'is a reference to inner data').to.shallow.equal(contextData.innerTestData); + expect(context.additionalData,'is a reference').to.shallow.equal(contextData.additionalData); + expect(context.lateAddition,'is a reference').to.shallow.equal(contextData.lateAddition); + expect(outerContext.testData,'has not been changed').to.shallow.equal(contextData.testData); + }); + }); + + script.experiment('second child experiment', () => { + + script.test('does not see mutations from a peer experiment', ({ context }) => { + + expect(context,'is deep equal').to.equal(outerContext); + expect(context,'is a shallow clone').to.not.shallow.equal(outerContext); + expect(context.testData,'is a reference').to.shallow.equal(contextData.testData); + }); + }); + }); + + const notebook = await Lab.execute(script, {}, null); + expect(notebook.tests.length,'has 5 tests').to.equal(5); + expect(notebook.failures,'has 0 failures').to.equal(0); + }); + it('nullifies test context on finish', async () => { const script = Lab.script({ schedule: false });