Skip to content

Commit d678f1c

Browse files
committed
feat(recipes): update recipe for creating testable side effects to use new createEffects function
1 parent c3d9122 commit d678f1c

5 files changed

Lines changed: 276 additions & 264 deletions

File tree

Lines changed: 170 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,170 @@
1-
# Recipe: creating testable side effects
2-
3-
Most web applications contain code which has [side effects](<https://en.wikipedia.org/wiki/Side_effect_(computer_science)>), e.g. changing the document title for the current browser tab or communicating over the network. Traditionally, this kind of code is difficult to test. **simplux** makes the concept of effects a first-class citizen and thereby provides a solution to the testing issue. This recipe shows you how **simplux** helps you create testable side effects.
4-
5-
> You can play with the code for this recipe in this [code sandbox](https://codesandbox.io/s/github/MrWolfZ/simplux/tree/master/recipes/advanced/creating-testable-side-effects).
6-
7-
Before we start let's install **simplux**.
8-
9-
```sh
10-
npm i @simplux/core @simplux/testing -S
11-
```
12-
13-
Now we're ready to go.
14-
15-
Let's start with two very simple side effects: reading and setting the document title of the current browser tab.
16-
17-
```ts
18-
// by default the returned function simply calls the provided function
19-
const getDocumentTitle = createEffect(() => document.title)
20-
21-
const setDocumentTitle = createEffect((title: string) => {
22-
document.title = title
23-
})
24-
```
25-
26-
These simple looking effects provide some interesting challenges for testing. There are two scenarios to consider:
27-
28-
#### Testing an effect itself
29-
30-
For our effects above you will either need to write an integration test that runs in a browser and really reads and sets the title. Or you will need to run the test in an environment that mocks the `document` and allows you to assert the title was set correctly, although this just moves the responsibility for mocking to a different library or framework. Since the effects are so simple you may also decide to omit testing them at all and instead focus on the more interesting scenario below.
31-
32-
#### Testing other code that uses an effect
33-
34-
This is where **simplux** can make your life simple. Each **simplux** effect can be mocked with the help of the `mockEffect` function from the testing package. Let's say we have a function to show a notification count in the tab title.
35-
36-
```ts
37-
const prefixDocumentTitleWithNotificationCount = (count: number) => {
38-
const currentTitle = getDocumentTitle()
39-
const prefixedTitle = `(${count}) ${currentTitle}`
40-
setDocumentTitle(prefixedTitle)
41-
}
42-
```
43-
44-
Without **simplux** it would be difficult to test this function. However, since we can mock effects it becomes quite simple.
45-
46-
```ts
47-
import { mockEffect } from '@simplux/testing'
48-
49-
describe('prefixDocumentTitleWithNotificationCount', () => {
50-
it('prefixes the current title with the given count', () => {
51-
const currentTitle = 'test title'
52-
53-
// after this line the provided function will be called instead
54-
// of the real effect until the mock is cleared
55-
mockEffect(getDocumentTitle, () => currentTitle)
56-
57-
// for convenience `mockEffect` returns the mock function as the
58-
// first item in the returned tuple (the second item is a callback
59-
// that clears the mock)
60-
const [setTitleMock] = mockEffect(setDocumentTitle, jest.fn())
61-
62-
// now we can safely call our function without it causing any
63-
// undesired side effects
64-
prefixDocumentTitleWithNotificationCount(5)
65-
66-
// and we can assert the function worked correctly
67-
expect(setTitleMock).toHaveBeenCalledWith('(5) test title')
68-
})
69-
})
70-
```
71-
72-
The `mockEffect` call above mocks our effect indefinitely (or until it is manually cleared via the second item in the returned tuple). The testing package provides a way to clear all simplux mocks which we can simply do after each test.
73-
74-
```ts
75-
import { clearAllSimpluxMocks } from '@simplux/testing'
76-
77-
afterEach(clearAllSimpluxMocks)
78-
```
79-
80-
Now let's go one step further and assume we have some code (maybe a UI component) that calls the `prefixDocumentTitleWithNotificationCount`. To test that code you would always have to mock both `getDocumentTitle` and `setDocumentTitle`, which can become quite noisy. However, as you probably already guessed, there is an alternative approach: make `prefixDocumentTitleWithNotificationCount` an effect itself.
81-
82-
```ts
83-
const prefixDocumentTitleWithNotificationCount = createEffect((count: number) => {
84-
const currentTitle = getDocumentTitle()
85-
const prefixedTitle = `(${count}) ${currentTitle}`
86-
setDocumentTitle(prefixedTitle)
87-
})
88-
```
89-
90-
This allows us to mock it directly where necessary without having to mock the lower level effects. We can see a pattern emerging here:
91-
92-
> Create minimal low level effects that are either integration tested or not tested due to their simplicity. Then build more complex yet unit testable higher level effects based on the lower level ones.
93-
94-
Let's apply this pattern for another very common scenario: loading data from a web API. We start with a minimal low level effect for making HTTP `GET` calls.
95-
96-
```ts
97-
const httpGet = createEffect(
98-
<T>(url: string): Promise<T> => {
99-
// ...use whatever library you prefer for making HTTP calls; use that library's
100-
// testing capabilities to test this effect
101-
},
102-
)
103-
```
104-
105-
Now let's say we have a simple **simplux** module for managing a collection of books.
106-
107-
```ts
108-
interface Book {
109-
id: string
110-
title: string
111-
author: string
112-
}
113-
114-
const booksModule = createSimpluxModule<Book[]>('books', [])
115-
116-
const books = {
117-
...booksModule,
118-
...createMutations(booksModule, {
119-
setAll: (_, books: Book[]) => books,
120-
}),
121-
}
122-
```
123-
124-
We want to populate the module with data from our API.
125-
126-
```ts
127-
const loadBooksFromApi = createEffect(async (authorFilter: string) => {
128-
const result = await httpGet<Book[]>(`https://my.domain.com/books?authorFilter=${authorFilter}`)
129-
books.setAll(result)
130-
return result
131-
})
132-
```
133-
134-
Thanks to **simplux** this effect is simple to test since we can mock both the `httpGet` effect as well as the `setAll` mutation ([this recipe](../testing-code-using-mutations#readme) will help you if you are unfamiliar with mocking mutations). At the same time, all code that uses this effect (e.g. a UI component) is also easy to test.
135-
136-
```ts
137-
import { clearAllSimpluxMocks, mockEffect, mockMutation } from '@simplux/testing'
138-
139-
describe('loading books from the API', () => {
140-
afterEach(clearAllSimpluxMocks)
141-
142-
it('uses the correct URL', () => {
143-
const mockData: Book[] = []
144-
const httpGetMock = jest.fn().mockReturnValue(Promise.resolve(mockData))
145-
mockEffect(httpGet, httpGetMock)
146-
mockMutation(setAll, jest.fn())
147-
148-
loadBooksFromApi('Tolkien')
149-
150-
expect(httpGetMock).toHaveBeenCalledWith('https://my.domain.com/books?authorFilter=Tolkien')
151-
})
152-
153-
// ... see the code of this recipe for a full list of tests
154-
})
155-
```
156-
157-
And that shows you how simple it is to test your side effects with the help of **simplux**.
158-
159-
Have a look at our [other recipes](../../../../..#recipes) to learn how **simplux** can help you make your life simple in other situations.
1+
# Recipe: creating testable side effects
2+
3+
Most web applications contain code which has [side effects](<https://en.wikipedia.org/wiki/Side_effect_(computer_science)>), e.g. changing the document title for the current browser tab or communicating over the network. Traditionally, this kind of code is difficult to test. **simplux** makes the concept of effects a first-class citizen and thereby provides a solution to the testing issue. This recipe shows you how **simplux** helps you create testable side effects.
4+
5+
> You can play with the code for this recipe in this [code sandbox](https://codesandbox.io/s/github/MrWolfZ/simplux/tree/master/recipes/advanced/creating-testable-side-effects).
6+
7+
Before we start let's install **simplux**.
8+
9+
```sh
10+
npm i @simplux/core @simplux/testing -S
11+
```
12+
13+
Now we're ready to go.
14+
15+
Let's start with two very simple side effects: reading and setting the document title of the current browser tab.
16+
17+
```ts
18+
// by default the returned function simply calls the provided function
19+
const getDocumentTitle = createEffect(() => document.title)
20+
21+
const setDocumentTitle = createEffect((title: string) => {
22+
document.title = title
23+
})
24+
```
25+
26+
We can also define multiple effects at once.
27+
28+
```ts
29+
const { getDocumentTitle, setDocumentTitle } = createEffects({
30+
getDocumentTitle: () => document.title,
31+
setDocumentTitle: (title: string) => {
32+
document.title = title
33+
},
34+
})
35+
```
36+
37+
These simple looking effects provide some interesting challenges for testing. There are two scenarios to consider:
38+
39+
#### Testing an effect itself
40+
41+
For our effects above you will either need to write an integration test that runs in a browser and really reads and sets the title. Or you will need to run the test in an environment that mocks the `document` and allows you to assert the title was set correctly, although this just moves the responsibility for mocking to a different library or framework. Since the effects are so simple you may also decide to omit testing them at all and instead focus on the more interesting scenario below.
42+
43+
#### Testing other code that uses an effect
44+
45+
This is where **simplux** can make your life simple. Each **simplux** effect can be mocked with the help of the `mockEffect` function from the testing package. Let's say we have a function to show a notification count in the tab title.
46+
47+
```ts
48+
const prefixDocumentTitleWithNotificationCount = (count: number) => {
49+
const currentTitle = getDocumentTitle()
50+
const prefixedTitle = `(${count}) ${currentTitle}`
51+
setDocumentTitle(prefixedTitle)
52+
}
53+
```
54+
55+
Without **simplux** it would be difficult to test this function. However, since we can mock effects it becomes quite simple.
56+
57+
```ts
58+
import { mockEffect } from '@simplux/testing'
59+
60+
describe('prefixDocumentTitleWithNotificationCount', () => {
61+
it('prefixes the current title with the given count', () => {
62+
const currentTitle = 'test title'
63+
64+
// after this line the provided function will be called instead
65+
// of the real effect until the mock is cleared
66+
mockEffect(getDocumentTitle, () => currentTitle)
67+
68+
// for convenience `mockEffect` returns the mock function as the
69+
// first item in the returned tuple (the second item is a callback
70+
// that clears the mock)
71+
const [setTitleMock] = mockEffect(setDocumentTitle, jest.fn())
72+
73+
// now we can safely call our function without it causing any
74+
// undesired side effects
75+
prefixDocumentTitleWithNotificationCount(5)
76+
77+
// and we can assert the function worked correctly
78+
expect(setTitleMock).toHaveBeenCalledWith('(5) test title')
79+
})
80+
})
81+
```
82+
83+
The `mockEffect` call above mocks our effect indefinitely (or until it is manually cleared via the second item in the returned tuple). The testing package provides a way to clear all simplux mocks which we can simply do after each test.
84+
85+
```ts
86+
import { clearAllSimpluxMocks } from '@simplux/testing'
87+
88+
afterEach(clearAllSimpluxMocks)
89+
```
90+
91+
Now let's go one step further and assume we have some code (maybe a UI component) that calls the `prefixDocumentTitleWithNotificationCount`. To test that code you would always have to mock both `getDocumentTitle` and `setDocumentTitle`, which can become quite noisy. However, as you probably already guessed, there is an alternative approach: make `prefixDocumentTitleWithNotificationCount` an effect itself.
92+
93+
```ts
94+
const prefixDocumentTitleWithNotificationCount = createEffect((count: number) => {
95+
const currentTitle = getDocumentTitle()
96+
const prefixedTitle = `(${count}) ${currentTitle}`
97+
setDocumentTitle(prefixedTitle)
98+
})
99+
```
100+
101+
This allows us to mock it directly where necessary without having to mock the lower level effects. We can see a pattern emerging here:
102+
103+
> Create minimal low level effects that are either integration tested or not tested due to their simplicity. Then build more complex yet unit testable higher level effects based on the lower level ones.
104+
105+
Let's apply this pattern for another very common scenario: loading data from a web API. We start with a minimal low level effect for making HTTP `GET` calls.
106+
107+
```ts
108+
const http = createEffects({
109+
get: <T>(url: string): Promise<T> => {
110+
// ...use whatever library you prefer for making HTTP calls; use that library's
111+
// testing capabilities to test this effect
112+
},
113+
})
114+
```
115+
116+
Now let's say we have a simple **simplux** module for managing a collection of books.
117+
118+
```ts
119+
interface Book {
120+
id: string
121+
title: string
122+
author: string
123+
}
124+
125+
const booksModule = createSimpluxModule<Book[]>('books', [])
126+
127+
const books = {
128+
...booksModule,
129+
...createMutations(booksModule, {
130+
setAll: (_, books: Book[]) => books,
131+
}),
132+
}
133+
```
134+
135+
We want to populate the module with data from our API.
136+
137+
```ts
138+
const loadBooksFromApi = createEffect(async (authorFilter: string) => {
139+
const result = await http.get<Book[]>(`https://my.domain.com/books?authorFilter=${authorFilter}`)
140+
books.setAll(result)
141+
return result
142+
})
143+
```
144+
145+
Thanks to **simplux** this effect is simple to test since we can mock both the `http.get` effect as well as the `setAll` mutation ([this recipe](../testing-code-using-mutations#readme) will help you if you are unfamiliar with mocking mutations). At the same time, all code that uses this effect (e.g. a UI component) is also easy to test.
146+
147+
```ts
148+
import { clearAllSimpluxMocks, mockEffect, mockMutation } from '@simplux/testing'
149+
150+
describe('loading books from the API', () => {
151+
afterEach(clearAllSimpluxMocks)
152+
153+
it('uses the correct URL', () => {
154+
const mockData: Book[] = []
155+
const httpGetMock = jest.fn().mockReturnValue(Promise.resolve(mockData))
156+
mockEffect(http.get, httpGetMock)
157+
mockMutation(setAll, jest.fn())
158+
159+
loadBooksFromApi('Tolkien')
160+
161+
expect(httpGetMock).toHaveBeenCalledWith('https://my.domain.com/books?authorFilter=Tolkien')
162+
})
163+
164+
// ... see the code of this recipe for a full list of tests
165+
})
166+
```
167+
168+
And that shows you how simple it is to test your side effects with the help of **simplux**.
169+
170+
Have a look at our [other recipes](../../../../..#recipes) to learn how **simplux** can help you make your life simple in other situations.

0 commit comments

Comments
 (0)