Skip to content

Commit

Permalink
Shallow clone context (#993)
Browse files Browse the repository at this point in the history
  • Loading branch information
nlf authored Sep 21, 2020
1 parent 2348c6b commit a3ad7ab
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 10 deletions.
48 changes: 40 additions & 8 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand All @@ -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');
});
});
});
```

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,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

Expand Down Expand Up @@ -393,7 +393,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) {
Expand Down
91 changes: 91 additions & 0 deletions test/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down

0 comments on commit a3ad7ab

Please sign in to comment.