Skip to content

Commit 67002f5

Browse files
committed
feat(recipes): add recipe for "testing my side effects"
1 parent ce6d013 commit 67002f5

11 files changed

Lines changed: 336 additions & 3 deletions

File tree

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,87 @@
11
# Recipe: testing side effects
22

3-
This recipe gives you some advice for testing side effects.
3+
This recipe gives you some advice on how to test your side effects (like loading data from your API).
44

5-
If you are new to **simplux** there is [a recipe](../../basics/getting-started#readme) that will help you get started before you follow this recipe.
5+
If you are new to **simplux** there is [a recipe](../../basics/getting-started#readme) that will help you get started before you follow this recipe. The recipe for [performing side effects](../performing-side-effects#readme) is also important for following this recipe.
66

77
> You can play with the code for this recipe in this [code sandbox](https://codesandbox.io/s/github/MrWolfZ/simplux/tree/master/recipes/advanced/testing-side-effects).
88
9-
This recipe is still a work-in-progress.
9+
Before we start let's install all the packages we need.
10+
11+
```sh
12+
npm i @simplux/core @simplux/testing redux -S
13+
```
14+
15+
Now we're ready to go.
16+
17+
For this recipe we use a simple scenario: loading data from an API. Let's create a module for this.
18+
19+
```ts
20+
interface Todo {
21+
id: string
22+
description: string
23+
isDone: boolean
24+
}
25+
26+
interface TodoState {
27+
[id: string]: Todo
28+
}
29+
30+
const initialState: TodoState = {}
31+
32+
const todosModule = createSimpluxModule({
33+
name: 'todos',
34+
initialState,
35+
})
36+
37+
const { setTodoItems } = createMutations(todosModule, {
38+
setTodoItems(state, items: Todo[]) {
39+
for (const id of Object.keys(state)) {
40+
delete state[id]
41+
}
42+
43+
for (const item of items) {
44+
state[item.id] = item
45+
}
46+
},
47+
})
48+
```
49+
50+
We want to load the todo items from our API. As we learned in the recipe for [performing side effects](../performing-side-effects#readme) we can create an effect for this.
51+
52+
```ts
53+
// this effect first calls an HTTP API and then performs some post-processing
54+
// on the client (in a typical application this post processing would most
55+
// likely already be done in the API but it serves as a good example for this
56+
// recipe)
57+
const loadTodosFromApi = createEffect(async (includeDoneItems: boolean) => {
58+
await loadItemsViaHttp() // to be implemented, see below
59+
60+
// do some post processing
61+
return todos.filter(t => !t.isDone || includeDoneItems)
62+
})
63+
```
64+
65+
How are we going to test this and what exactly are we even testing here? There are two parts: 1) the call to the HTTP API, and 2) the post processing logic. 2) is the real logic that we should test and 1) is something that we certainly do not want to execute during our test, so we should mock it. Depending on your tech stack the library you use for making HTTP calls probably already provides a way to mock HTTP calls, in which case we recommend you use that library's testing capabilities. However, alternatively we could (and I am sure you have already guessed this) just make `loadItemsViaHttp` an effect itself.
66+
67+
```ts
68+
const loadItemsViaHttp = createEffect(async () => {
69+
// call the API
70+
})
71+
```
72+
73+
This way we can use **simplux**'s mocking capabilities to mock this call. If you go this route you could also create lower-level generic effects for HTTP calls, e.g. for `GET` calls:
74+
75+
```ts
76+
const get = createEffect(async (url: string) => {
77+
// call the API
78+
})
79+
```
80+
81+
Once we have 1) mocked we can easily test the post-processing logic from 2) by simply calling the `loadTodosFromApi`.
82+
83+
> There are alternative designs to the effect above that would allow testing the filtering logic without the effect, e.g. by extracting it into a separate function. How you want to structure your effects is completely up to you.
84+
85+
And that how simple it is to test your side effects with the help of **simplux**.
86+
87+
Have a look at our [other recipes](../../../../..#recipes) to learn how **simplux** can help you make your life simple in other situations.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="">
3+
<head>
4+
<meta charset="UTF-8" />
5+
</head>
6+
<body>
7+
Include done items?
8+
<input type="checkbox" id="includeDoneItemsCheckbox" />
9+
<br />
10+
<button id="loadDataBtn">Load data</button>
11+
<ul id="itemList"></ul>
12+
</body>
13+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
roots: ['<rootDir>/src', '<rootDir>'],
3+
moduleFileExtensions: ['js', 'ts'],
4+
transform: {
5+
'^.+\\.ts': 'ts-jest',
6+
},
7+
testMatch: ['<rootDir>/src/**/*.spec.ts'],
8+
testPathIgnorePatterns: ['node_modules'],
9+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@simplux/recipes.advanced.testing-side-effects",
3+
"version": "0.9.0-alpha.0",
4+
"description": "This recipe gives you some advice on how to test your side effects (like loading data from your API)",
5+
"main": "src/index.ts",
6+
"scripts": {
7+
"start": "webpack-dev-server --mode development --progress --color",
8+
"test": "jest --forceExit --verbose --detectOpenHandles --no-cache --colors --runInBand --config=./jest.config.js"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/MrWolfZ/simplux.git"
13+
},
14+
"bugs": {
15+
"url": "https://github.com/MrWolfZ/simplux/issues"
16+
},
17+
"homepage": "https://github.com/MrWolfZ/simplux/tree/master/recipes/advanced/testing-side-effects#readme",
18+
"keywords": [
19+
"recipe",
20+
"redux",
21+
"simplux",
22+
"effects",
23+
"testing",
24+
"typescript"
25+
],
26+
"author": "Jonathan Ziller <jonathan.ziller@gmail.com> (https://www.github.com/MrWolfZ)",
27+
"license": "MIT",
28+
"private": true,
29+
"dependencies": {
30+
"@simplux/core": "0.9.0-alpha.0",
31+
"@simplux/testing": "0.9.0-alpha.0",
32+
"redux": "^4.0.1"
33+
},
34+
"devDependencies": {
35+
"@types/jest": "^24.0.18",
36+
"html-webpack-plugin": "^3.2.0",
37+
"jest": "24.8.0",
38+
"ts-jest": "^24.0.2",
39+
"ts-loader": "^6.0.2",
40+
"typescript": "^3.5.1",
41+
"webpack": "^4.32.2",
42+
"webpack-cli": "^3.3.2",
43+
"webpack-dev-server": "^3.5.1"
44+
}
45+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"infiniteLoopProtection": true,
3+
"hardReloadOnChange": false,
4+
"view": "tests"
5+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// this code is part of the simplux recipe "testing my side effects":
2+
// https://github.com/MrWolfZ/simplux/tree/master/recipes/advanced/testing-side-effects
3+
4+
import { createEffect } from '@simplux/core'
5+
import { Todo } from './todos'
6+
7+
export const loadItemsViaHttp = createEffect(async () => {
8+
await new Promise(resolve => setTimeout(resolve, 200))
9+
10+
return [
11+
{ id: '1', description: 'go shopping', isDone: false },
12+
{ id: '2', description: 'clean house', isDone: true },
13+
{ id: '3', description: 'bring out trash', isDone: true },
14+
{ id: '4', description: 'go to the gym', isDone: false },
15+
] as Todo[]
16+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// this code is part of the simplux recipe "testing my side effects":
2+
// https://github.com/MrWolfZ/simplux/tree/master/recipes/advanced/testing-side-effects
3+
4+
import { clearAllSimpluxMocks, mockEffect } from '@simplux/testing'
5+
import { loadItemsViaHttp } from './api'
6+
import { loadTodosFromApi, Todo } from './todos'
7+
8+
describe('loading todo items', () => {
9+
it('calls the HTTP API', async () => {
10+
// since we use an effect for calling the HTTP API we can simply test
11+
// whether it was called inside our effect or not
12+
const loadDataMock = jest.fn().mockReturnValue(Promise.resolve([]))
13+
mockEffect(loadItemsViaHttp, loadDataMock)
14+
15+
await loadTodosFromApi(true)
16+
17+
expect(loadDataMock).toHaveBeenCalled()
18+
})
19+
20+
it('filters out done items if requested', async () => {
21+
const data: Todo[] = [
22+
{
23+
id: '1',
24+
description: 'clean',
25+
isDone: false,
26+
},
27+
{
28+
id: '2',
29+
description: 'shopping',
30+
isDone: true,
31+
},
32+
]
33+
34+
// to test only the filtering logic of the effect, we mock the lower-level
35+
// effect with the appropriate data
36+
const loadDataMock = jest.fn().mockReturnValue(Promise.resolve(data))
37+
mockEffect(loadItemsViaHttp, loadDataMock)
38+
39+
const result = await loadTodosFromApi(false)
40+
41+
expect(result).toEqual([data[0]])
42+
})
43+
44+
afterEach(clearAllSimpluxMocks)
45+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// this code is part of the simplux recipe "testing my side effects":
2+
// https://github.com/MrWolfZ/simplux/tree/master/recipes/advanced/testing-side-effects
3+
4+
import { loadTodosFromApi, setTodoItems } from './todos'
5+
6+
if (document.getElementById('loadDataBtn')) {
7+
setupEventHandler()
8+
} else {
9+
document.addEventListener('DOMContentLoaded', setupEventHandler)
10+
}
11+
12+
export function setupEventHandler() {
13+
document
14+
.getElementById('loadDataBtn')!
15+
.addEventListener('click', async () => {
16+
const inputElement = document.getElementById(
17+
'includeDoneItemsCheckbox',
18+
) as HTMLInputElement
19+
const value = inputElement.checked
20+
21+
const items = await onLoadButtonClicked(value)
22+
23+
const itemList = document.getElementById('itemList') as HTMLUListElement
24+
let child = itemList.lastElementChild
25+
while (child) {
26+
itemList.removeChild(child)
27+
child = itemList.lastElementChild
28+
}
29+
30+
for (const { id, description } of items) {
31+
const newItemElement = document.createElement('li')
32+
newItemElement.id = id
33+
newItemElement.innerHTML = description
34+
itemList.appendChild(newItemElement)
35+
}
36+
})
37+
}
38+
39+
export async function onLoadButtonClicked(includeDoneItems: boolean) {
40+
const todos = await loadTodosFromApi(includeDoneItems)
41+
setTodoItems(todos)
42+
return todos
43+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// this code is part of the simplux recipe "testing my side effects":
2+
// https://github.com/MrWolfZ/simplux/tree/master/recipes/advanced/testing-side-effects
3+
4+
import {
5+
createEffect,
6+
createMutations,
7+
createSimpluxModule,
8+
} from '@simplux/core'
9+
import { loadItemsViaHttp } from './api'
10+
11+
export interface Todo {
12+
id: string
13+
description: string
14+
isDone: boolean
15+
}
16+
17+
export interface TodoState {
18+
[id: string]: Todo
19+
}
20+
21+
const initialState: TodoState = {}
22+
23+
export const todosModule = createSimpluxModule({
24+
name: 'todos',
25+
initialState,
26+
})
27+
28+
export const { setTodoItems } = createMutations(todosModule, {
29+
setTodoItems(state, items: Todo[]) {
30+
for (const id of Object.keys(state)) {
31+
delete state[id]
32+
}
33+
34+
for (const item of items) {
35+
state[item.id] = item
36+
}
37+
},
38+
})
39+
40+
// this is the effect we want to test; this effect consists of two
41+
// parts, 1) the data fetching and 2) some post-processing logic;
42+
// to test this effect we should mock 1) to test the logic of 2)
43+
export const loadTodosFromApi = createEffect(
44+
async (includeDoneItems: boolean) => {
45+
// 1) data fetching
46+
const todos = await loadItemsViaHttp()
47+
48+
// 2) post-processing logic
49+
return todos.filter(t => !t.isDone || includeDoneItems)
50+
},
51+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"sourceMap": true,
4+
"strict": true,
5+
"noUnusedLocals": true,
6+
"noUnusedParameters": true,
7+
"lib": ["dom", "es2015"],
8+
"types": ["node", "jest"]
9+
}
10+
}

0 commit comments

Comments
 (0)