Skip to content

Commit 10c826b

Browse files
authored
Add Unit Test Mocking Support (#60)
* Basic unit test mock implementation * Add test for mocking multiple records * Mocking simpleQuery/Mutation and docs * Update docs * Build * Fix docs * setupTestUtils method
1 parent f1da97e commit 10c826b

File tree

20 files changed

+980
-45
lines changed

20 files changed

+980
-45
lines changed

dist/vuex-orm-graphql.esm.js

Lines changed: 219 additions & 24 deletions
Large diffs are not rendered by default.

docs/.vuepress/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ themeConfig:
3131
- /guide/eager-loading/
3232
- /guide/virtual-fields/
3333
- /guide/meta-fields/
34+
- /guide/testing/
3435
- /guide/faq/

docs/guide/custom-queries/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ mutation SendSms($to: string!, $text: string!) {
193193
}
194194
}`;
195195

196-
const result = store.dispatch('entities/simpleQuery', {
196+
const result = await store.dispatch('entities/simpleMutation', {
197197
query,
198198
variables: { to: '+4912345678', text: 'GraphQL is awesome!' }
199199
});

docs/guide/persist/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ the `persist` action.
99
Via calling
1010

1111
```javascript
12-
const post = await Post.create({
12+
await Post.create({ data: {
1313
content: 'Lorem Ipsum dolor sit amet',
1414
title: 'Example Post',
15-
user: user.query().first()
16-
});
15+
user: User.query().first()
16+
}});
17+
18+
const post = Post.query().first();
1719

1820
await post.$persist();
1921
// or

docs/guide/testing/README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Testing
2+
3+
[[toc]]
4+
5+
## Unit Testing
6+
7+
To unit test vue components which use the persistence actions of this plugin, you need to mock
8+
the results of the GraphQL queries. The GraphQL plugin offers some utils to do this.
9+
10+
First we have to import the mock method from the test utils via
11+
12+
```js
13+
import VuexORMGraphQL from '@vuex-orm/plugin-graphql';
14+
import { setupTestUtils, mock } from '@vuex-orm/plugin-graphql/lib/test-utils';
15+
```
16+
17+
After that we have to setup the test utils, this is very easy, just pass the imported VuexORMGraphQL
18+
plugin like this:
19+
20+
```
21+
setupTestUtils(VuexORMGraphQL);
22+
```
23+
24+
Now we're ready to go. In the next step we can setup mocks via
25+
26+
```js
27+
mock('fetch').for(User).andReturn({ id: 1, name: 'Charlie Brown' });
28+
```
29+
30+
This means: Whenever `User.fetch()` is called, insert `{ id: 1, name: 'Charlie Brown' }` in the Vuex-ORM
31+
store.
32+
33+
The mock method also accepts a second param which allows to match specific calls. Only those
34+
properties which are within the given object are considered while matching. Example:
35+
36+
```js
37+
// Will only trigger when `User.fetch(17)` is called
38+
mock('fetch', { filter: { id: 17 } }).for(User).andReturn({ id: 17, name: 'Charlie Brown' });
39+
40+
// Will only trigger when `User.fetch({ filter: { active: true }})` is called
41+
mock('fetch', { filter: { active: true } }).for(User).andReturn([
42+
{ id: 17, name: 'Charlie Brown' },
43+
{ id: 18, name: 'Snoopy' }
44+
]);
45+
```
46+
47+
Additionally the argument of `andReturn` can be a function, which will be called each time the mock
48+
is triggered.
49+
50+
The following examples describe how each action type can be mocked.
51+
52+
53+
### Fetch
54+
55+
```js
56+
// This mock call
57+
mock('fetch', { filter: { id: 42 }}).for(User).andReturn(userData);
58+
59+
// will be triggerd by
60+
User.fetch(42);
61+
```
62+
63+
### Persist
64+
65+
```js
66+
// This mock call
67+
mock('persist', { id: 17 }).for(User).andReturn({ id: 17, name: 'Charlie Brown' });
68+
69+
// will be triggerd by
70+
user.$persist();
71+
```
72+
73+
### Push
74+
75+
```js
76+
// This mock call
77+
mock('push', { data: { ... } }).for(User).andReturn({ id: 17, name: 'Charlie Brown' });
78+
79+
// will be triggerd by
80+
user.$push();
81+
```
82+
83+
### Destroy
84+
85+
```js
86+
// This mock call
87+
mock('destroy', { id: 17 }).for(User).andReturn({ id: 17, name: 'Charlie Brown' });
88+
89+
// will be triggerd by
90+
user.$destroy();
91+
```
92+
93+
### Query
94+
95+
96+
97+
### Mutate
98+
99+
```js
100+
// This mock call
101+
mock('mutate', { name: 'upvote', args: { id: 4 }}).for(Post).andReturn({ ... });
102+
103+
// will be triggerd by
104+
post.$mutate({ name: 'upvote' });
105+
```
106+
107+
### Simple Query
108+
109+
Mocking simple queries works slightly different then the other actions, because these are not model
110+
related. Thus we have to mock these globally by omiting the model (`for`):
111+
112+
```js
113+
// This mock call
114+
mock('simpleQuery', { name: 'example' }).andReturn({ success: true });
115+
116+
// will be triggered by
117+
store.dispatch('entities/simpleQuery', { query: 'query example { success }' });
118+
```
119+
120+
### Simple Mutation
121+
122+
Works just like the simple queries:
123+
124+
```js
125+
// This mock call
126+
mock('simpleMutation', {
127+
name: 'SendSms',
128+
variables: { to: '+4912345678', text: 'GraphQL is awesome!' }
129+
}).andReturn({ sendSms: { delivered: true }});
130+
131+
// will be triggered by
132+
const query = `
133+
mutation SendSms($to: string!, $text: string!) {
134+
sendSms(to: $to, text: $text) {
135+
delivered
136+
}
137+
}`;
138+
139+
const result = await store.dispatch('entities/simpleMutation', {
140+
query,
141+
variables: { to: '+4912345678', text: 'GraphQL is awesome!' }
142+
});
143+
```
144+
145+
146+
### Resetting a mock
147+
148+
::: warning
149+
Support for resetting a mock is currently work in progress and will be added soon.
150+
151+
See https://github.com/vuex-orm/plugin-graphql/issues/61
152+
:::
153+
154+
155+
## Misc
156+
157+
The testing utils also provide some other useful functions, which are listed here:
158+
159+
- `async clearORMStore()`: Will remove all records from the Vuex-ORM store to clean up while testing.
160+
161+
162+
## Integration Testing
163+
164+
::: warning
165+
Support for integration testing is currently work in progress and will be added soon.
166+
167+
See https://github.com/vuex-orm/plugin-graphql/issues/59
168+
:::

src/actions/destroy.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ActionParams } from '../support/interfaces';
22
import Action from './action';
33
import NameGenerator from '../graphql/name-generator';
4+
import { Store } from '../orm/store';
45

56
/**
67
* Destroy action for sending a delete mutation. Will be used for record.$destroy().
@@ -17,6 +18,13 @@ export default class Destroy extends Action {
1718
const model = this.getModelFromState(state);
1819
const mutationName = NameGenerator.getNameForDestroy(model);
1920

21+
const mockReturnValue = model.$mockHook('destroy', { id });
22+
23+
if (mockReturnValue) {
24+
await Store.insertData(mockReturnValue, dispatch);
25+
return true;
26+
}
27+
2028
args = this.prepareArgs(args, id);
2129

2230
await Action.mutation(mutationName, args, dispatch, model);

src/actions/fetch.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ export default class Fetch extends Action {
1818
*/
1919
public static async call ({ state, dispatch }: ActionParams, params?: ActionParams): Promise<Data> {
2020
const context = Context.getInstance();
21-
await context.loadSchema();
22-
2321
const model = this.getModelFromState(state);
2422

23+
const mockReturnValue = model.$mockHook('fetch', {
24+
filter: params ? params.filter || {} : {}
25+
});
26+
27+
if (mockReturnValue) {
28+
return Store.insertData(mockReturnValue, dispatch);
29+
}
30+
31+
await context.loadSchema();
32+
2533
// Filter
2634
const filter = params && params.filter ?
2735
Transformer.transformOutgoingData(model, params.filter, Object.keys(params.filter)) : {};

src/actions/mutate.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ActionParams, Arguments, Data } from '../support/interfaces';
22
import Action from './action';
33
import Context from '../common/context';
44
import Schema from '../graphql/schema';
5+
import { Store } from '../orm/store';
56

67
/**
78
* Mutate action for sending a custom mutation. Will be used for Model.mutate() and record.$mutate().
@@ -18,8 +19,18 @@ export default class Mutate extends Action {
1819
public static async call ({ state, dispatch }: ActionParams, { args, name }: ActionParams): Promise<Data> {
1920
if (name) {
2021
const context: Context = Context.getInstance();
21-
const schema: Schema = await context.loadSchema();
2222
const model = this.getModelFromState(state);
23+
24+
const mockReturnValue = model.$mockHook('mutate', {
25+
name,
26+
args: args || {}
27+
});
28+
29+
if (mockReturnValue) {
30+
return Store.insertData(mockReturnValue, dispatch);
31+
}
32+
33+
const schema: Schema = await context.loadSchema();
2334
args = this.prepareArgs(args);
2435

2536
// There could be anything in the args, but we have to be sure that all records are gone through

src/actions/persist.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ActionParams, Data } from '../support/interfaces';
33
import Action from './action';
44
import NameGenerator from '../graphql/name-generator';
55
import Model from '../orm/model';
6+
import { Store } from '../orm/store';
67

78
/**
89
* Persist action for sending a create mutation. Will be used for record.$persist().
@@ -20,6 +21,17 @@ export default class Persist extends Action {
2021
const mutationName = NameGenerator.getNameForPersist(model);
2122
const oldRecord = model.getRecordWithId(id);
2223

24+
const mockReturnValue = model.$mockHook('persist', {
25+
id,
26+
args: args || {}
27+
});
28+
29+
if (mockReturnValue) {
30+
const newRecord = Store.insertData(mockReturnValue, dispatch);
31+
await this.deleteObsoleteRecord(model, newRecord, oldRecord);
32+
return newRecord;
33+
}
34+
2335
// Arguments
2436
args = this.prepareArgs(args);
2537
this.addRecordToArgs(args, model, oldRecord);

src/actions/push.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ActionParams, Data } from '../support/interfaces';
22
import Action from './action';
33
import NameGenerator from '../graphql/name-generator';
4+
import { Store } from '../orm/store';
45

56
/**
67
* Push action for sending a update mutation. Will be used for record.$push().
@@ -18,6 +19,15 @@ export default class Push extends Action {
1819
const model = this.getModelFromState(state);
1920
const mutationName = NameGenerator.getNameForPush(model);
2021

22+
const mockReturnValue = model.$mockHook('push', {
23+
data,
24+
args: args || {}
25+
});
26+
27+
if (mockReturnValue) {
28+
return Store.insertData(mockReturnValue, dispatch);
29+
}
30+
2131
// Arguments
2232
args = this.prepareArgs(args, data.id);
2333
this.addRecordToArgs(args, model, data);

0 commit comments

Comments
 (0)