Skip to content

Commit

Permalink
feat(vest): add conditional to skip
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed May 14, 2021
1 parent 65988bf commit dcd8ad2
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 180 deletions.
57 changes: 32 additions & 25 deletions packages/vest/docs/exclusion.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Excluding and including tests

When performing validations in real world-scenarios, you may need to only run tests of a single field in your suite, or skip certain tests according to some logic. That's why Vest includes `vest.skip()` and `vest.only()`.
When performing validations in real world-scenarios, you may need to only run tests of a single field in your suite, or skip certain tests according to some logic. That's why Vest includes `skip()` and `only()`.

`vest.skip()` and `vest.only()` are functions that take a name of the test, or a list of names to either include or exclude fields from being validated. They should be called from the body of suite callback, and in order for them to take effect, they should be called before anything else.
`skip()` and `only()` are functions that take a name of the test, or a list of names to either include or exclude fields from being validated. They should be called from the body of suite callback, and in order for them to take effect, they should be called before anything else.

!> **NOTE** When using `vest.only()` or `vest.skip()` you must place them before any of the tests defined in the suite. Hooks run in order of appearance, which means that if you place your `skip` hook after the field you're skipping - it won't have any effect.
!> **NOTE** When using `only()` or `skip()` you must place them before any of the tests defined in the suite. Hooks run in order of appearance, which means that if you place your `skip` hook after the field you're skipping - it won't have any effect.

### Only running specific tests (including)

When validating upon user interactions, you will usually want to only validate the input the user currently interacts with to prevent errors appearing in unrelated places. For this, you can use `vest.only()` with the name of the test currently being validated.
When validating upon user interactions, you will usually want to only validate the input the user currently interacts with to prevent errors appearing in unrelated places. For this, you can use `only()` with the name of the test currently being validated.

In the example below, we're assuming the argument `fieldName` is being populated with the name of the field we want to test. If none is passed, the call to `only` will be ignored, and all tests will run as usual. This allows us to test each field at a time during the interaction, but test all on form submission.

```js
import vest, { enforce, test } from 'vest';
import vest, { enforce, test, only } from 'vest';

const suite = vest.create('New User', (data, fieldName) => {
vest.only(fieldName);
only(fieldName);

test('username', 'Username is invalid', () => {
/* some validation logic*/
Expand All @@ -36,13 +36,13 @@ const validationResult = suite(formData, changedField);

There are not many cases for skipping tests, but they do exist. For example, when you wish to prevent validation of a promo code when none provided.

In this case, and in similar others, you can use `vest.skip()`. When called, it will only skip the specified fields, all other tests will run as they should.
In this case, and in similar others, you can use `skip()`. When called, it will only skip the specified fields, all other tests will run as they should.

```js
import vest, { enforce, test } from 'vest';
import vest, { enforce, test, skip } from 'vest';

const suite = vest.create('purchase', data => {
if (!data.promo) vest.skip('promo');
if (!data.promo) skip('promo');

// this test won't run when data.promo is falsy.
test('promo', 'Promo code is invalid', () => {
Expand All @@ -55,39 +55,46 @@ const validationResult = suite(formData);

## Conditionally excluding portions of the suite

In some cases we might need to skip a test or a group based on a given condition. In these cases, we may find it easier to use vest.skipWhen which takes a boolean expression and a callback with the tests to run. This is better than simply wrapping the tests with an if/else statement because they can are still listed in the suite result as skipped.
In some cases we might need to skip a test or a group based on a given condition, for example - based on the intermediate state of the currently running suite. To allow this, we need to alter the `skip` function a bit.

This relies on adding an additional **first** parameter to skip: `shouldSkip` function. The shouldSkip function returns a boolean and determines whether the tests in the following callback should be skipped or not. We also need to use a callback containing tests as the second argument.

This is better than simply wrapping the tests with an if/else statement because they can are still listed in the suite result as skipped.

In the following example we're skipping the server side verification of the username if the username is invalid to begin with:

```js
import vest, { test, enforce } from 'vest';
import vest, { test, enforce, skip } from 'vest';

const suite = vest.create('user_form', (data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotEmpty();
});

vest.skipWhen(suite.get().hasErrors('password'), () => {
test('username', 'Username already exists', () => {
// this is an example for a server call
return doesUserExist(data.username);
});
});
skip(
() => suite.get().hasErrors('password'), // only skip if this is truthy
() => {
test('username', 'Username already exists', () => {
// this is an example for a server call
return doesUserExist(data.username);
});
}
);
});
export default suite;
```

## Including and excluding groups of tests

Similar to the way you use `vest.skip` and `vest.only` to include and exclude tests, you can use `vest.skip.group` and `vest.only.group` to exclude and include whole groups.
Similar to the way you use `skip` and `only` to include and exclude tests, you can use `skip.group` and `only.group` to exclude and include whole groups.

These two functions are very powerful and give you control of whole portions of your suite at once.

```js
import vest, { test, group, enforce } from 'vest';
import vest, { test, group, enforce, skip } from 'vest';

vest.create('authentication_form', data => {
vest.skip.group(data.userExists ? 'signUp' : 'signIn');
skip.group(data.userExists ? 'signUp' : 'signIn');

test('userName', "Can't be empty", () => {
enforce(data.username).isNotEmpty();
Expand Down Expand Up @@ -116,11 +123,11 @@ vest.create('authentication_form', data => {

## Things to know about how these functions work:

**vest.only.group()**:
When using `vest.only.group`, other groups won't be tested - but top level tests that aren't nested in any groups will. The reasoning is that the top level space is a shared are that will always be executed. If you want only your group to run, nest everything else under groups as well.
**only.group()**:
When using `only.group`, other groups won't be tested - but top level tests that aren't nested in any groups will. The reasoning is that the top level space is a shared are that will always be executed. If you want only your group to run, nest everything else under groups as well.

If you combine `vest.only.group` with `vest.skip`, if you skip a field inside a group that is included, that field will be excluded during this run regardless of its group membership.
If you combine `only.group` with `skip`, if you skip a field inside a group that is included, that field will be excluded during this run regardless of its group membership.

**vest.skip.group()**
**skip.group()**

If you combine `vest.skip.group` with `vest.only` your included field declared within the skipped tests will be ignored.
If you combine `skip.group` with `only` your included field declared within the skipped tests will be ignored.
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ Object {
"only": [Function],
"optional": [Function],
"skip": [Function],
"skipWhen": [Function],
"test": [Function],
"warn": [Function],
}
Expand Down

This file was deleted.

126 changes: 53 additions & 73 deletions packages/vest/src/hooks/__tests__/exclusive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,85 +225,65 @@ describe('exclusive hooks', () => {
expect(res).toEqual(false);
});
});
});

describe('`skipWhen` hook', () => {
let suite, fn1, fn2, fn3;
describe('conditional skip', () => {
describe('When falsy', () => {
it('Should run passed fields', () => {
const result = vest.create(faker.lorem.word(), () => {
vest.skip(() => false, ['field1']);

vest.test('field1', () => false);
vest.test('field2', () => false);
})();
expect(result.tests.field1.testCount).toBe(1);
expect(result.tests.field2.testCount).toBe(1);
});

beforeEach(() => {
fn1 = jest.fn(() => false);
fn2 = jest.fn(() => false);
fn3 = jest.fn(() => false);
suite = vest.create(shouldSkip => {
vest.skipWhen(shouldSkip, () => {
vest.test('field1', fn1);
vest.group('group', () => {
vest.test('field2', fn2);
describe('skip callback', () => {
it('Should run tests in the callback', () => {
const result = vest.create(faker.lorem.word(), () => {
vest.skip(
() => false,
() => {
vest.test('field1', () => false);
}
);

vest.test('field2', () => false);
})();
expect(result.tests.field1.testCount).toBe(1);
expect(result.tests.field2.testCount).toBe(1);
});
});
vest.test('field3', fn3);
});
});

describe('When `shouldSkip` is `true`', () => {
it('Should skip all tests within the callback', () => {
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(fn3).not.toHaveBeenCalled(); // sanity
suite(true);
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(fn3).toHaveBeenCalled(); // sanity
});

it('Should list all skipped tests as skipped', () => {
const res = suite(true);
expect(res.tests).toMatchInlineSnapshot(`
Object {
"field1": Object {
"errorCount": 0,
"testCount": 0,
"warnCount": 0,
},
"field2": Object {
"errorCount": 0,
"testCount": 0,
"warnCount": 0,
},
"field3": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
}
`);
expect(res.groups).toMatchInlineSnapshot(`
Object {
"group": Object {
"field2": Object {
"errorCount": 0,
"testCount": 0,
"warnCount": 0,
},
},
}
`);
});
});

describe('When `shouldSkip` is `false`', () => {
it('Should run all tests', () => {
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(fn3).not.toHaveBeenCalled();
suite(false);
expect(fn1).toHaveBeenCalled();
expect(fn2).toHaveBeenCalled();
expect(fn3).toHaveBeenCalled();
});
describe('When truthy', () => {
it('Should only register - and not run passed fields', () => {
const result = vest.create(faker.lorem.word(), () => {
vest.skip(() => true, ['field1']);

vest.test('field1', () => false);
vest.test('field2', () => false);
})();
expect(result.tests.field1.testCount).toBe(0);
expect(result.tests.field2.testCount).toBe(1);
});

it('Should produce correct validation result', () => {
expect(suite(false)).toMatchSnapshot();
describe('skip callback', () => {
it('Should only register - and not run tests inside the callback', () => {
const result = vest.create(faker.lorem.word(), () => {
vest.skip(
() => true,
() => {
vest.test('field1', () => false);
}
);

vest.test('field2', () => false);
})();
expect(result.tests.field1.testCount).toBe(0);
expect(result.tests.field2.testCount).toBe(1);
});
});
});
});
});
Expand Down
34 changes: 19 additions & 15 deletions packages/vest/src/hooks/exclusive.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EXCLUSION_ITEM_TYPE_GROUPS,
} from 'runnableTypes';
import throwError from 'throwError';
import withArgs from 'withArgs';

/**
* Adds a field or multiple fields to inclusion group.
Expand All @@ -24,25 +25,28 @@ only.group = item =>

/**
* Adds a field or multiple fields to exclusion group.
* @param {String[]|String} item Item to be added to exclusion group.
* @param {() => boolean} [shouldSkip] An optional callback determining whether "skip" should be applied
* @param {String[]|String|Function} item Item to be added to exclusion group.
*/
export function skip(item) {
return addTo(EXCLUSION_GROUP_NAME_SKIP, EXCLUSION_ITEM_TYPE_TESTS, item);
}
export const skip = withArgs(function (args) {
const [item, shouldSkip] = args.reverse();
let skip = true;

skip.group = item =>
addTo(EXCLUSION_GROUP_NAME_SKIP, EXCLUSION_ITEM_TYPE_GROUPS, item);
if (isFunction(shouldSkip)) {
skip = !!shouldSkip();
}

/**
* Conditionally skips nested test callbacks
* @param {boolean} shouldSkip
* @param {Function} callback
*/
export function skipWhen(shouldSkip, callback) {
if (isFunction(callback)) {
context.run({ skip: !!shouldSkip }, () => callback());
if (isFunction(item)) {
return context.run({ skip }, () => item());
}
}

if (skip) {
return addTo(EXCLUSION_GROUP_NAME_SKIP, EXCLUSION_ITEM_TYPE_TESTS, item);
}
});

skip.group = item =>
addTo(EXCLUSION_GROUP_NAME_SKIP, EXCLUSION_ITEM_TYPE_GROUPS, item);

/**
* Checks whether a certain test profile excluded by any of the exclusion groups.
Expand Down
2 changes: 1 addition & 1 deletion packages/vest/src/hooks/hooks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { only, skip, skipWhen } from 'exclusive';
export { only, skip } from 'exclusive';
export { default as warn } from 'warn';
export { default as group } from 'group';
export { default as optional } from 'optionalTests';

0 comments on commit dcd8ad2

Please sign in to comment.