|
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