Skip to content

Commit 7b71b0e

Browse files
feat(overmind): improve testing approach
1 parent c9b4901 commit 7b71b0e

File tree

7 files changed

+109
-83
lines changed

7 files changed

+109
-83
lines changed

packages/node_modules/overmind/src/index.ts

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,11 @@ function deepCopy(obj) {
7878

7979
export function createMock<Config extends Configuration>(
8080
config: Config,
81-
effectsCallback?: (
82-
effect: {
83-
path: string
84-
args: any[]
85-
},
86-
index: number
87-
) => any
81+
mockedEffects?: Partial<Config['effects']>
8882
): {
8983
actions: ResolveMockActions<Config['actions']>
9084
state: ResolveState<Config['state']>
9185
} {
92-
let effectCount = 0
93-
let effectsCalled = []
9486
const mock = new Overmind(
9587
Object.assign({}, config, {
9688
state: deepCopy(config.state),
@@ -99,28 +91,25 @@ export function createMock<Config extends Configuration>(
9991
devtools: false,
10092
testMode: {
10193
effectsCallback: (effect) => {
102-
if (!effectsCallback) {
103-
return
94+
const mockedEffect = (effect.name
95+
? effect.name.split('.')
96+
: []
97+
).reduce((aggr, key) => (aggr ? aggr[key] : aggr), mockedEffects)
98+
99+
if (!mockedEffect || (mockedEffect && !mockedEffect[effect.method])) {
100+
throw new Error(
101+
`The effect "${effect.name}" with metod ${
102+
effect.method
103+
} has not been mocked`
104+
)
104105
}
105-
106-
return effectsCallback(
107-
{
108-
path: effect.name + '.' + effect.method,
109-
args: effect.args,
110-
},
111-
effectCount++
112-
)
106+
return mockedEffect[effect.method]({
107+
path: effect.name + '.' + effect.method,
108+
args: effect.args,
109+
})
113110
},
114111
actionCallback: (execution) => {
115-
const effects = effectsCalled.slice()
116-
117-
effectCount = 0
118-
effectsCalled.length = 0
119-
120-
return {
121-
mutations: execution.flush().mutations,
122-
effects,
123-
}
112+
return execution.flush().mutations
124113
},
125114
},
126115
}

packages/node_modules/overmind/src/internalTypes.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,7 @@ type NestedMockActions =
200200
}
201201
| undefined
202202

203-
type MockResult = {
204-
mutations: IMutation[]
205-
effects: ({
206-
path: string
207-
args: any[]
208-
})[]
209-
}
203+
type MockResult = IMutation[]
210204

211205
export type ResolveMockActions<
212206
Actions extends NestedMockActions

packages/node_modules/overmind/src/mock.test.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,40 @@ describe('Mock', () => {
1313
const state: State = {
1414
foo: 'bar',
1515
}
16-
const test: Action = ({ state }) => {
17-
state.foo = 'bar2'
16+
const test: Action = ({ state, effect }) => {
17+
state.foo = effect()
1818
}
1919
const actions = { test }
20+
const effect = () => 'bar2'
21+
const effects = { effect }
2022
const config = {
2123
state,
2224
actions,
25+
effects,
2326
}
2427

2528
type Config = {
2629
state: typeof state
2730
actions: typeof actions
31+
effects: typeof effects
2832
}
2933

3034
type Action<Input = void> = TAction<Config, Input>
3135

32-
const mock = createMock(config)
36+
const mock = createMock(config, {
37+
effect() {
38+
return 'bar3'
39+
},
40+
})
3341

3442
return mock.actions.test().then((result) =>
35-
expect(result).toEqual({
36-
mutations: [
37-
{
38-
method: 'set',
39-
path: 'foo',
40-
args: ['bar2'],
41-
},
42-
],
43-
effects: [],
44-
})
43+
expect(result).toEqual([
44+
{
45+
method: 'set',
46+
path: 'foo',
47+
args: ['bar3'],
48+
},
49+
])
4550
)
4651
})
4752
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export default () => [
2+
{
3+
fileName: 'overmind/actions.test.ts',
4+
code: `
5+
import { createMock } from 'overmind'
6+
import { config } from './'
7+
8+
describe('Actions', () => {
9+
describe('getPost', () => {
10+
test('should get post with passed id', async () => {
11+
const mock = createMock(config, {
12+
api: {
13+
getPost(id) {
14+
return Promise.resolve({
15+
id
16+
})
17+
}
18+
}
19+
})
20+
21+
const mutations = await actions.getPost('1')
22+
23+
expect(mutations).toMatchSnapshot()
24+
})
25+
test('should handle errors', async () => {
26+
const mock = createMock(config, {
27+
api = {
28+
getPost() {
29+
throw new Error('test')
30+
}
31+
}
32+
})
33+
34+
const mutations = await actions.getPost('1')
35+
36+
expect(mutations).toMatchSnapshot()
37+
})
38+
})
39+
})
40+
`,
41+
},
42+
]

packages/overmind-website/examples/guide/writingtests/actiontest.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,43 @@ export default () => [
22
{
33
fileName: 'overmind/actions.test.ts',
44
code: `
5-
import * as actions from './actions'
5+
import { createMock } from 'overmind'
6+
import { config } from './'
67
78
describe('Actions', () => {
89
describe('getPost', () => {
910
test('should get post with passed id', async () => {
10-
const state = {}
11-
const api = {
12-
getPost(id) {
13-
return Promise.resolve({
14-
id
15-
})
11+
const mock = createMock(config, {
12+
api: {
13+
getPost(id) {
14+
return Promise.resolve({
15+
id
16+
})
17+
}
1618
}
17-
}
19+
})
1820
1921
await actions.getPost('1')
2022
21-
expect(state.isLoadingPost).toBe(false)
22-
expect(state.currentPost).toEqual({ id: '1' })
23+
expect(mock.state).toEqual({
24+
isLoadingPost: false,
25+
currentPost: { id: '1' },
26+
error: null
27+
})
2328
})
2429
test('should handle errors', async () => {
25-
const state = {}
26-
const api = {
27-
getPost() {
28-
throw new Error('test')
30+
const mock = createMock(config, {
31+
api = {
32+
getPost() {
33+
throw new Error('test')
34+
}
2935
}
30-
}
36+
})
3137
3238
await actions.getPost('1')
3339
34-
expect(state.isLoadingPost).toBe(false)
35-
expect(state.error.message).toBe('test')
40+
expect(mock.state.isLoadingPost).toBe(false)
41+
expect(mock.state.error.message).toBe('test')
3642
})
3743
})
3844
})

packages/overmind-website/examples/guide/writingtests/effecttest.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ export default () => [
22
{
33
fileName: 'overmind/effects.test.ts',
44
code: `
5-
import * as effects from './effects'
5+
import { Api } from './effects'
66
77
describe('Effects', () => {
88
describe('Api', () => {
9-
test('should get a post', async () => {
9+
test('should get a post using baseUrl and authToken in header', async () => {
1010
expect.assertions(3)
11-
const api = new effects.Api({
11+
const api = new Api({
1212
get(url, config) {
1313
expect(url).toBe('/test/posts/1')
1414
expect(config).toEqual({

packages/overmind-website/guides/intermediate/05_writingtests.md

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Writing tests
22

3-
Testing is a broad subject and everybody has an opinion about it. We can only show you how we think about testing in general and how to effectively write those tests for your Overmind app. The benefit of Overmind is that you only test pure functions. That means there is no trickery to testing actions or operators. Your effects can also easily be made testable. These kinds of tests is what you would call **unit tests**. Since these unit tests covers the intention of each piece of logic we do not consider any need for writing integration tests on the logic. If you want to test how your application works when it is all put together we recommend doing integration tests as close to the user experience as possible. Testing solutions like [Cypress.io](https://www.cypress.io/) is a great way to do integration tests as close to the user experience as possible.
3+
Testing is a broad subject and everybody has an opinion on it. We can only show you how we think about testing in general and how to effectively write those tests for your Overmind app. It is encouraged to think **unit testing** of actions and effects. This will cover expected changes in state and that your side effects behaves in a predictable manner. If you want to test how your application works when it is all put together we recommend doing integration tests as close to the user experience as possible. Testing solutions like [Cypress.io](https://www.cypress.io/) is a great way to do exactly that.
44

55
## Testing actions
66

7-
Since all effects are "injected" into actions you can easily stub them out. That means if you for example have an action that looks like this:
7+
When testing an action you want to verify that changes to state are performed as expected. To give you the best possible testing experience Overmind has mocking tool called **createMock**. It takes your application configuration and allows you to run actions as if they were run from components.
88

99
```marksy
1010
h(Example, { name: "guide/writingtests/action.ts" })
@@ -18,23 +18,13 @@ h(Example, { name: "guide/writingtests/actiontest.ts" })
1818

1919
If your actions can result in multiple scenarios a unit test is beneficial. But you will be surprised how straight forward the logic of your actions will become. Since effects are encouraged to be application specific you will most likely be testing those more than you will test any action.
2020

21-
## Testing operators
22-
23-
Operators are based on the [op-op specification](https://github.com/christianalfoni/op-op-spec), which means that they actually have a different signature when being called. This is not something you think about writing your application code, but when testing single operators it is important to get it right.
24-
25-
So imagine you have an operator like:
26-
27-
```marksy
28-
h(Example, { name: "guide/writingtests/operator.ts" })
29-
```
30-
31-
You might want to test that it does what you want it to:
21+
You do not have to explicitly write the expected state. You can also use for example [jest]() for snapshot testing. The action will return a list of mutations performed and effects run. This is perfect for snapshot testing.
3222

3323
```marksy
34-
h(Example, { name: "guide/writingtests/operatortest.ts" })
24+
h(Example, { name: "guide/writingtests/actionsnapshot.ts" })
3525
```
3626

37-
You can even do tests of pipes that are composed of multiple operators. You just pass in a stubbed context and check it on the *complete* callback (the second one).
27+
In this scenario we would also ensure that the **isLoadingPost** state indeed flipped to *true* before moving to *false* at the end.
3828

3929
## Testing effects
4030

0 commit comments

Comments
 (0)