react-obsidian 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/src/index.d.ts +1 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +4 -1
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/testkit/index.d.ts +6 -1
  6. package/dist/testkit/index.d.ts.map +1 -1
  7. package/dist/testkit/index.js +11 -17
  8. package/dist/testkit/index.js.map +1 -1
  9. package/dist/testkit/mockGraphs.d.ts +4 -0
  10. package/dist/testkit/mockGraphs.d.ts.map +1 -0
  11. package/dist/testkit/mockGraphs.js +23 -0
  12. package/dist/testkit/mockGraphs.js.map +1 -0
  13. package/dist/testkit/mockModel.d.ts +3 -0
  14. package/dist/testkit/mockModel.d.ts.map +1 -0
  15. package/dist/testkit/mockModel.js +14 -0
  16. package/dist/testkit/mockModel.js.map +1 -0
  17. package/package.json +1 -1
  18. package/src/index.ts +1 -0
  19. package/testkit/index.ts +11 -14
  20. package/testkit/mockGraphs.ts +20 -0
  21. package/testkit/mockModel.ts +10 -0
  22. package/.buildkite/pipeline.yml +0 -10
  23. package/.eslintignore +0 -1
  24. package/.eslintrc.json +0 -105
  25. package/.vscode/settings.json +0 -6
  26. package/babel.config.js +0 -13
  27. package/documentation/README.md +0 -41
  28. package/documentation/babel.config.js +0 -3
  29. package/documentation/blog/2019-05-28-first-blog-post.md +0 -12
  30. package/documentation/blog/2019-05-29-long-blog-post.md +0 -44
  31. package/documentation/blog/2021-08-01-mdx-blog-post.mdx +0 -20
  32. package/documentation/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  33. package/documentation/blog/2021-08-26-welcome/index.md +0 -25
  34. package/documentation/blog/authors.yml +0 -17
  35. package/documentation/docs/documentation/documentation.mdx +0 -190
  36. package/documentation/docs/documentation/installation.mdx +0 -56
  37. package/documentation/docs/documentation/meta/clearingGraphs.mdx +0 -13
  38. package/documentation/docs/documentation/meta/middlewares.mdx +0 -27
  39. package/documentation/docs/documentation/usage/ClassComponents.mdx +0 -18
  40. package/documentation/docs/documentation/usage/Classes.mdx +0 -41
  41. package/documentation/docs/documentation/usage/FunctionalComponents.mdx +0 -57
  42. package/documentation/docs/documentation/usage/Graphs.mdx +0 -154
  43. package/documentation/docs/documentation/usage/Hooks.mdx +0 -85
  44. package/documentation/docs/documentation/usage/Reactivity.mdx +0 -116
  45. package/documentation/docs/documentation/usage/ServiceLocator.mdx +0 -38
  46. package/documentation/docs/documentation/usage/_category_.json +0 -9
  47. package/documentation/docs/guides/configurableApplications.mdx +0 -205
  48. package/documentation/docs/guides/mockDependencies.mdx +0 -141
  49. package/documentation/docusaurus.config.js +0 -151
  50. package/documentation/package-lock.json +0 -22975
  51. package/documentation/package.json +0 -47
  52. package/documentation/sidebars.js +0 -34
  53. package/documentation/src/components/HomepageFeatures/index.tsx +0 -71
  54. package/documentation/src/components/HomepageFeatures/styles.module.css +0 -11
  55. package/documentation/src/css/custom.css +0 -30
  56. package/documentation/src/pages/index.module.css +0 -23
  57. package/documentation/src/pages/index.tsx +0 -41
  58. package/documentation/src/pages/playground/index.mdx +0 -21
  59. package/documentation/src/theme/SearchBar.js +0 -19
  60. package/documentation/static/.nojekyll +0 -0
  61. package/documentation/static/img/api.svg +0 -101
  62. package/documentation/static/img/favicon.ico +0 -0
  63. package/documentation/static/img/logo.svg +0 -265
  64. package/documentation/static/img/obsidian.png +0 -0
  65. package/documentation/static/img/prototype.svg +0 -1
  66. package/documentation/static/img/stethoscope.svg +0 -37
  67. package/documentation/tsconfig.json +0 -7
  68. package/documentation/yarn.lock +0 -8167
  69. package/example/.buckconfig +0 -6
  70. package/example/.editorconfig +0 -3
  71. package/example/.eslintrc.js +0 -4
  72. package/example/.flowconfig +0 -65
  73. package/example/.gitattributes +0 -3
  74. package/example/.prettierrc.js +0 -7
  75. package/example/.watchmanconfig +0 -1
  76. package/example/App.tsx +0 -31
  77. package/example/MyInjectedComponent.tsx +0 -37
  78. package/example/__tests__/App-test.js +0 -14
  79. package/example/android/app/BUCK +0 -55
  80. package/example/android/app/build.gradle +0 -227
  81. package/example/android/app/build_defs.bzl +0 -19
  82. package/example/android/app/debug.keystore +0 -0
  83. package/example/android/app/proguard-rules.pro +0 -10
  84. package/example/android/app/src/debug/AndroidManifest.xml +0 -13
  85. package/example/android/app/src/debug/java/com/example/ReactNativeFlipper.java +0 -72
  86. package/example/android/app/src/main/AndroidManifest.xml +0 -25
  87. package/example/android/app/src/main/java/com/example/MainActivity.java +0 -15
  88. package/example/android/app/src/main/java/com/example/MainApplication.java +0 -80
  89. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  90. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  91. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  92. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  93. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  94. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  95. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  96. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  97. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  98. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  99. package/example/android/app/src/main/res/values/strings.xml +0 -3
  100. package/example/android/app/src/main/res/values/styles.xml +0 -8
  101. package/example/android/build.gradle +0 -38
  102. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  103. package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -5
  104. package/example/android/gradle.properties +0 -28
  105. package/example/android/gradlew +0 -185
  106. package/example/android/gradlew.bat +0 -89
  107. package/example/android/settings.gradle +0 -3
  108. package/example/app.json +0 -4
  109. package/example/babel.config.js +0 -8
  110. package/example/index.js +0 -9
  111. package/example/ios/Podfile +0 -30
  112. package/example/ios/Podfile.lock +0 -526
  113. package/example/ios/example/AppDelegate.h +0 -8
  114. package/example/ios/example/AppDelegate.m +0 -62
  115. package/example/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json +0 -38
  116. package/example/ios/example/Images.xcassets/Contents.json +0 -6
  117. package/example/ios/example/Info.plist +0 -55
  118. package/example/ios/example/LaunchScreen.storyboard +0 -47
  119. package/example/ios/example/main.m +0 -9
  120. package/example/ios/example.xcodeproj/project.pbxproj +0 -689
  121. package/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme +0 -88
  122. package/example/ios/example.xcworkspace/contents.xcworkspacedata +0 -10
  123. package/example/ios/example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  124. package/example/ios/exampleTests/Info.plist +0 -24
  125. package/example/ios/exampleTests/exampleTests.m +0 -65
  126. package/example/metro.config.js +0 -44
  127. package/example/package.json +0 -35
  128. package/example/tsconfig.json +0 -42
  129. package/global.d.ts +0 -1
  130. package/jest.config.js +0 -17
  131. package/jest.setup-after-env.js +0 -4
  132. package/tsconfig.base.json +0 -42
  133. package/tsconfig.json +0 -3
  134. package/tsconfig.prod.json +0 -7
@@ -1,116 +0,0 @@
1
- ---
2
- sidebar_position: 5
3
- title: "Reactivity"
4
- ---
5
-
6
- Obsidian is first and foremost a dependency injection library. But it also includes reactive programming features that allow you to observe changes in your data and react to them. This is useful for things like updating the UI when a value changes.
7
-
8
- ## Observable
9
- Making a class field reactive is as simple as wrapping it with the `Observable` decorator. Observable is a data holder classes that also allows you to subscribe to changes in the data.
10
-
11
- ### Create observables
12
- In the example below, we declare a boolean observable called `isLoggedIn`. We can then subscribe to changes in the `isLoggedIn` value and update the UI accordingly.
13
-
14
- ```ts
15
- import { Observable } from 'react-obsidian';
16
-
17
- class UserService {
18
- public isLoggedIn = new Observable(false); // The initial value is false
19
- }
20
-
21
- export default new UserService();
22
- ```
23
-
24
- ### Observe changes
25
- #### Observe changes in hooks or components
26
- Once you have declared an observable, you can subscribe to changes in the value by using the `useObserver` hook. This hook will return the current value of the observable and subscribe to changes in the value. When the value changes, the hook will re-render the component.
27
-
28
- ```ts
29
- import { useObserver } from 'react-obsidian';
30
- import userService from './UserService';
31
-
32
- const useLogin = () => {
33
- const [isLoggedIn] = useObserver(userService.isLoggedIn);
34
-
35
- return {isLoggedIn};
36
- }
37
- ```
38
-
39
- #### Observe changes imperatively
40
- You can also subscribe to changes in an observable imperatively by calling the `subscribe` method on the observable. This method returns a function that can be called to unsubscribe from the observable.
41
-
42
- ```ts
43
- import userService from './UserService';
44
-
45
- const unsubscribe = userService.isLoggedIn.subscribe((isLoggedIn: boolean) => {
46
- // Do something with the isLoggedIn value
47
- });
48
- ```
49
-
50
- ### Update Observables
51
- Observables expose the data they hold via a `value` property. You can update the value of an observable by setting the value of the `value` property.
52
- This will also trigger any subscribers to the observable.
53
-
54
- ```ts
55
- import userService from './UserService';
56
-
57
- userService.isLoggedIn.value = true; // Update the value and notify all subscribers
58
- ```
59
-
60
- #### Update Observables from hooks or components
61
- The `useObserver` hook also returns a setter function that can be used to update the value of the observable.
62
-
63
- ```ts
64
- import { useObserver } from 'react-obsidian';
65
- import userService from './UserService';
66
-
67
- const useLogin = () => {
68
- const [isLoggedIn, setIsLoggedIn] = useObserver(userService.isLoggedIn);
69
- const onLogoutButtonPress = useCallback(() => {
70
- setIsLoggedIn(false);
71
- }, [setIsLoggedIn]);
72
-
73
- return {isLoggedIn, onLogoutButtonPress};
74
- }
75
- ```
76
-
77
- ### Avoid recreating the initial observable
78
- When using the `useObserver` hook, it is important to avoid recreating the initial observable.
79
-
80
- ```ts title="Avoid instantiating observables in hooks"
81
- const useLogin = () => {
82
- const [isLoggedIn] = useObserver(new Observable(false));
83
- }
84
- ```
85
-
86
- Even if the value of the observable is the same, this can cause unexpected behavior since it's instantiated on every render.
87
-
88
- To solve this, you can pass a generator function to the useObserver hook instead:
89
- ```ts
90
- const useLogin = () => {
91
- const [isLoggedIn] = useObserver(() => new Observable(false));
92
- }
93
- ```
94
-
95
- If you pass a function to the useObserver hook, it will only be called on the first render. This ensures that the observable is only instantiated once.
96
-
97
- ### Merge multiple observable sources
98
- `MediatorObservable` is a special type of observable that allows you to merge multiple observable sources into a single observable. This is useful for creating side effect from one or more observables.
99
-
100
- In the example below, we create a `MediatorObservable` called `downloadStatus` that will be updated when either the `networkConditions` or `batteryLevel` observables are updated. We can then subscribe to changes in the `downloadStatus` observable to update the UI or perform other side effects.
101
-
102
- ```ts
103
- import { MediatorObservable, Observable } from 'react-obsidian';
104
-
105
- const networkConditions = new Observable<'poor' | 'strong'>();
106
- const batteryLevel = new Observable<'low' | 'normal'>();
107
-
108
- const downloadStatus = new MediatorObservable<'paused' | 'active'>('active')
109
- .addSource(networkConditions, (condition: 'poor' | 'strong') => {
110
- this.value = condition === 'poor' ? 'paused' : 'active';
111
- })
112
- .addSource(batteryLevel, (level: 'low' | 'normal') => {
113
- this.value = level === 'low' ? 'paused' : 'active';
114
- });
115
-
116
- ```
@@ -1,38 +0,0 @@
1
- ---
2
- sidebar_position: 6
3
- title: "Service locator"
4
- ---
5
-
6
- ## Obtaining dependencies imperatively
7
-
8
- Obsidian is an Inversion of Control container. This means that it will automatically resolve dependencies for you, and you don't need to worry about how to obtain them. However, there are times when you need to obtain a dependency imperatively, for example when you need to pass a dependency to a third-party library that doesn't support dependency injection.
9
-
10
- For these cases, you can obtain a graph instance and access the dependencies it provides imperatively. This is done by using the `Obsidian.obtain()` function which allows you to treat the graph as a Service Locator.
11
-
12
- ### Example
13
- Consider the following graph which provides two dependencies: `fooService` and `barService` where `barService` depends on `fooService`:
14
-
15
- ```ts
16
- import {Singleton, Graph, ObjectGraph, Provides} from 'react-obsidian';
17
-
18
- @Singleton() @Graph()
19
- export class SomeGraph extends ObjectGraph {
20
- @Provides()
21
- fooService(): FooService {
22
- return new FooService();
23
- }
24
-
25
- @Provides()
26
- barService(fooService: FooService): AppInitializer {
27
- return new BarService(fooService);
28
- }
29
- }
30
- ```
31
-
32
- Obtaining the dependencies directly from the graph is straight forward:
33
- ```ts Obtaining a dependency imperatively
34
- Obsidian.obtain(ApplicationGraph).fooService();
35
-
36
- // Even though barService depends on fooService, you don't need to provide its dependency
37
- Obsidian.obtain(ApplicationGraph).barService();
38
- ```
@@ -1,9 +0,0 @@
1
- {
2
- "label": "Usage",
3
- "position": 2,
4
- "collapsed": false,
5
- "link": {
6
- "type": "generated-index",
7
- "description": "Learn how to inject hooks, components and classes with Obsidian."
8
- }
9
- }
@@ -1,205 +0,0 @@
1
- ---
2
- sidebar_position: 2
3
- title: 'Configurable applications'
4
- toc_max_heading_level: 4
5
- ---
6
-
7
- import Tabs from '@theme/Tabs';
8
- import TabItem from '@theme/TabItem';
9
-
10
- Designing applications to be flexible and configurable makes them more tolerable to changing requirements. The ability to change code frequently and quickly is one of the most important KPIs of any development team. This is generally made possible by a design that facilitates small pull requests, that modify a minimal amount of code across a minimal number of files.
11
-
12
- The Dependency Injection pattern helps us write flexible code that is more tolerable to change by addressing three key concerns:
13
-
14
- * How can a class be <ins>independent</ins> from the creation of the objects it depends on?
15
- * How can an application, and the objects it uses support different <ins>configurations</ins>?
16
- * How can the <ins>behavior</ins> of a piece of code be changed without editing it directly?
17
-
18
- In this article we will learn how Obsidian can help us address these concerns.
19
-
20
- ## Configuring applications with providers
21
- When using Obsidian, dependencies are declared and constructed in classes called Graphs. Each dependency is constructed by a method called a provider which acts as a **Seam**. Lets see how we can leverage them to make our apps flexible and configurable.
22
-
23
- <!-- These providers act as **Seams**. -->
24
-
25
- :::tip What are Seams?
26
- A seam is a place where you can alter behavior in your program without editing in that place.
27
-
28
- [Working Effectively with Legacy Code](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052) by Michael Feathers
29
- :::
30
-
31
- ### Example 1: Interchangeable dependencies according to external configurations
32
- In this example we'll learn how to change the concrete object returned by a provider according to an external configuration. In a real life scenario, the external configuration would represent an A/B test or a feature toggle.
33
-
34
- #### Step 1: Declare a graph
35
- Lets declare a simple graph that provides a single dependency: an HTTP client used to make network requests.
36
-
37
- ```ts
38
- @Singleton() @Graph()
39
- class ApplicationGraph extends ObjectGraph {
40
- @Provides()
41
- httpClient(): HttpClient {
42
- return new HttpClient();
43
- }
44
- }
45
- ```
46
-
47
- Our `HttpClient` is using the standard `fetch` API to make network requests and for the sake of simplicity, it only supports `GET` and `POST` requests.
48
-
49
- ```ts
50
- class HttpClient {
51
- async get(url: string): Promise<any> {
52
- const response = await fetch(url, { method: 'GET' });
53
- return await response.json();
54
- }
55
-
56
- async post(url: string, body: any): Promise<any> {
57
- const response = fetch(url, { method: 'POST', body: JSON.stringify(body) });
58
- return await response.json();
59
- }
60
- }
61
- ```
62
-
63
- #### Step 2: Implement another HTTP client
64
- Just like our current HTTP client, the new client will only support `GET` and `POST` requests. The only difference is that it will use the `axios` library to make network requests.
65
-
66
- ```ts
67
- class AxiosClient {
68
- async get(url: string): Promise<any> {
69
- const response = await axios.get(url);
70
- return response.data;
71
- }
72
-
73
- async post(url: string, body: any): Promise<any> {
74
- const response = await axios.post(url, body);
75
- return response.data;
76
- }
77
- }
78
- ```
79
-
80
- #### Step 3: Make the clients interchangeable
81
- To easily switch between the two clients, we'll use a well known principle called [Dependency Inversion](https://en.wikipedia.org/wiki/Dependency_inversion_principle). This principle states that high-level modules should not depend on low-level modules. Both the `HttpClient` and the `AxiosClient` are low-level modules, so we'll make the `ApplicationGraph` depend on an abstraction called `NetworkClient` instead.
82
-
83
- ```ts
84
- interface NetworkClient {
85
- get(url: string): Promise<any>;
86
- post(url: string, body: any): Promise<any>;
87
- }
88
- ```
89
-
90
- The two network clients will implement this interface:
91
- <Tabs>
92
- <TabItem value="http" label="HttpClient" default>
93
-
94
- ```ts
95
- class HttpClient implements NetworkClient {
96
- override async get(url: string): Promise<any> {
97
- const response = await fetch(url, { method: 'GET' });
98
- return await response.json();
99
- }
100
-
101
- override async post(url: string, body: any): Promise<any> {
102
- const response = fetch(url, { method: 'POST', body: JSON.stringify(body) });
103
- return await response.json();
104
- }
105
- }
106
- ```
107
-
108
- </TabItem>
109
- <TabItem value="axios" label="AxiosClient">
110
-
111
- ```ts
112
- class AxiosClient implements NetworkClient {
113
- override async get(url: string): Promise<any> {
114
- const response = await axios.get(url);
115
- return response.data.json;
116
- }
117
-
118
- override async post(url: string, body: any): Promise<any> {
119
- const response = await axios.post(url, body);
120
- return response.data.json;
121
- }
122
- }
123
- ```
124
- </TabItem>
125
- </Tabs>
126
-
127
- #### Step 4: Return the correct client according to the configuration
128
- To determine which client to return, we'll use a new dependency called `AppConfig` which will be used to access the application's configuration.
129
-
130
- ```ts
131
- @Singleton() @Graph()
132
- class ApplicationGraph extends ObjectGraph {
133
- @Provides()
134
- httpClient(appConfig: AppConfig): NetworkClient {
135
- return appConfig.shouldUseAxiosClient() ? new AxiosClient() : new HttpClient();
136
- }
137
-
138
- @Provides()
139
- appConfig(): AppConfig {
140
- return new AppConfig();
141
- }
142
- }
143
- ```
144
-
145
- We're done! Now we can easily control which network client to use according the application's configuration.
146
-
147
- #### Conclusion and after thoughts
148
- In this example we learned how to make dependencies interchangeable, and how to control which dependency to use according to an external configuration. This is a very common use case in large scale applications where we need an extra layer of assurance that changes can be easily rolled back in case of a bug.
149
-
150
- Two important things to note about this example:
151
- 1. The provider's return type was changed to `NetworkClient` instead of `HttpClient`. This change could lead to further changes in the codebase, but it's a small price to pay for the flexibility it provides.
152
- 2. We wanted to keep the example short and easy to follow, so the two HTTP clients are simplified implementations of an actual client. They also share the same API which made it easy to implement the `NetworkClient` interface and have the two clients implement it. If the two clients had different APIs, perhaps because they supported typed request options and responses, then we would have to create common interfaces that would represent the common parts of the two APIs and adapters that would convert the two clients' APIs to the common API and vice versa.
153
-
154
- ### Example 2: Mocking dependencies in acceptance/integration tests
155
- Acceptance and integration tests are a great way to test how an application behaves as a whole. In these type of tests, objects aren't mocked since we're testing how the objects behave when they interact with each other. But because tests also need to be predictable and stable, there are some operations that we do want to simulate. For example, we might want to mock a network client so that we don't make real network requests during the test as that would add an unwanted layer of unpredictability to the test.
156
-
157
- In this example we'll learn how to mock a dependency and how to use that mocked instance across all objects involved in the test.
158
-
159
- #### Step 1: Declare a graph
160
- As in the previous example, we'll declare a simple graph that provides a single dependency: an HTTP client used to make network requests.
161
-
162
- ```ts
163
- @Singleton() @Graph()
164
- export class ApplicationGraph extends ObjectGraph {
165
- @Provides()
166
- httpClient(): HttpClient {
167
- return new HttpClient();
168
- }
169
- }
170
- ```
171
-
172
- #### Step 2: Mock the HTTP client
173
- To provide a mocked HTTP client to all objects involved in the test, we'll create a new graph that extends the `ApplicationGraph` and overrides the `httpClient` provider. In the next step we'll learn how to use this graph in our tests.
174
-
175
- ```ts
176
- import { mock } from 'jest-mock-extended';
177
-
178
- @Singleton() @Graph()
179
- export class ApplicationGraphForTests extends ApplicationGraph {
180
- @Provides()
181
- override httpClient(): HttpClient {
182
- return mock<HttpClient>();
183
- }
184
- }
185
- ```
186
-
187
- #### Step 3: Use the graph in the test
188
- To use the graph in the test, we'll use Obsidian's test kit to use the `ApplicationGraphForTests` instead of the `ApplicationGraph` whenever it's needed.
189
-
190
- ```ts
191
- import {testKit} from 'react-obsidian';
192
-
193
- describe('Test suite', () => {
194
- beforeEach(() => {
195
- testKit.mockGraphs({
196
- // Instruct Obsidian to use the ApplicationGraphForTests instead of the ApplicationGraph
197
- ApplicationGraph: ApplicationGraphForTests,
198
- });
199
- });
200
-
201
- it('should do something', () => {
202
- // ...
203
- });
204
- });
205
- ```
@@ -1,141 +0,0 @@
1
- ---
2
- sidebar_position: 1
3
- title: 'Mocking dependencies in unit tests'
4
- ---
5
-
6
- Tests are an integral part of any software project. They let you verify that your code works as expected and that it doesn't break when you make changes. We want our tests to be as clear as possible so that developers don't have to waste time figuring out what the test is doing our how to fix it when it fails.
7
-
8
- Obsidian promotes Object Oriented design, a paradigm that focuses on the relationships between objects and how they interact with each other. In this article we'll learn how adopting this approach lets us mock objects more easily and as a result improve the readability and maintainability of our tests.
9
-
10
- :::tip On readable tests and developer velocity
11
- "Every time the developers have to stop and puzzle through a test to figure out what it means, they have less time left to spend on creating new features, and the team velocity drops."
12
-
13
- — [Growing Object-Oriented Software, Guided by Tests](https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627)
14
- :::
15
-
16
- ## The problem
17
- The setup phase of a test is often the most complex part of the test. It involves creating test data, mocking dependencies and instantiating the unit-under-test. We've identified three common problems that make tests brittle, difficult to maintain, and hard to understand:
18
- 1. **Partial mocks** - a unit test is meant to test a single unit of code in isolation. If a dependency is partially mocked, our test is no longer testing a single unit. A bug in the partially mocked dependency can cause this unit test to fail preventing us from quickly identifying the root cause of the failure.
19
- 2. **Dependencies are introduced implicitly to the unit-under-test, usually via imports** - we should always create valid objects. If an object depends on another object, we should pass that dependency explicity in through the constructor. The constructor serves as the contract for the dependencies that a class requires to function. There's no point in creating partially working classes, and the constructor is used to enforces this constraint.
20
- 3. **Manual mocks** - manually mocking dependencies is a tedious and error prone process. It's easy to forget to mock a dependency, or to mock it incorrectly.
21
-
22
- To illustrate these problems, let's look at a simple example.
23
-
24
- ```js showLineNumbers
25
- describe('Example', () => {
26
- const openURL = jest.fn();
27
-
28
- let logger;
29
- let foo;
30
- let uut;
31
-
32
- beforeEach(() => {
33
- // Problem 1: Partial mocks
34
- logger = require('./Logger');
35
- logger.log = jest.fn();
36
- const spy = jest.spyOn(logger, 'warn');
37
-
38
- // Problem 2: Implicit dependencies.
39
- // Our UUT uses Linking.openUrl so we mock it on the module level.
40
- jest.mock('react-native', () => ({
41
- Linking: {
42
- openURL
43
- },
44
- }));
45
- // Problem 3: Manual mocks
46
- foo = {
47
- doSomething: jest.fn(),
48
- }
49
- uut = new Example(logger, foo);
50
- });
51
- });
52
- ```
53
-
54
- ## The solution
55
- To achieve our goal of reducing boilerplate and improving readability, we'll refactor the above example as follows:
56
- 1. **Convert all dependencies to ES6 classes** - this will allow us to mock them using [jest-mock-extended](https://github.com/marchaos/jest-mock-extended) - a library that lets us create mock classes in a type-safe manner.
57
- 2. **Pass dependencies in through the constructor** - we'll pass the dependencies explicitly to the unit-under-test. This step will require us to declare new classes that will encapsulate interactions with third-party libraries.
58
-
59
- <!-- In Object Oriented design, logic is encapsulated in classes that are introduced (injected) to one another in through the constructor. -->
60
- <!-- This approach lets us use modern mocking techniques that require less boilerplate and are easier to reason about. -->
61
- <!-- To achieve our goal, we'll do two things:
62
- First,
63
- First, refactor our code to use dependency injection.
64
-
65
-
66
- To achieve our goal, we'll use an open-source library called [jest-mock-extended](https://github.com/marchaos/jest-mock-extended) which provides a `mock` function that creates a mock object with all the methods and properties of the original object. This means that we don't have to mock each method and property individually. -->
67
-
68
- ### Step 1: Encapsulate interactions with third-party dependencies
69
- Implicit dependencies (dependencies introduced by importing a module) make it difficult to reason about the code and to test it. To avoid this problem, we'll create a new class that encapsulates interactions with the third-party library. We'll see how this approach lets us mock dependencies more easily.
70
-
71
- ```ts title="Encapsulating the Linking module in a new class responsible for opening URLs"
72
- import {Linking} from 'react-native';
73
-
74
- export class UrlOpener {
75
- public async openUrl(url: string) {
76
- if (await Linking.canOpenURL(url)) {
77
- await Linking.openURL(url);
78
- } else {
79
- throw new Error(`Can't open URL: ${url}`);
80
- }
81
- }
82
- }
83
- ```
84
-
85
- :::tip On decoupling third party dependencies
86
- "Avoid littering direct calls to library classes in your code. You might think that you'll never change them, but that can become a self-fulfilling prophecy."
87
-
88
- — [Working Effectively with Legacy Code](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052)
89
- :::
90
-
91
- ### Step 2: Convert dependencies to ES6 classes
92
- We'll convert the `Logger` and `Foo` classes to ES6 classes:
93
-
94
- ```ts title="Logger.ts"
95
- export class Logger {
96
- public log(message: string) {
97
- console.log(message);
98
- }
99
-
100
- public warn(message: string) {
101
- console.warn(message);
102
- }
103
- }
104
- ```
105
-
106
- ```ts title="Foo.ts"
107
- export class Foo {
108
- public doSomething() {
109
- console.log('doing something...');
110
- setTimeout(() => {
111
- console.log('done!');
112
- }, 1000);
113
- }
114
- }
115
- ```
116
-
117
- ### Step 3: Mock dependencies using jest-mock-extended
118
- When we mock a dependency using `jest-mock-extended`, we get a mock object that has all the methods and properties of the original object. This means that we don't have to mock each method and property individually. And, because we eliminated the implicit dependency on the `Linking` module, we can use this approach to mock it as well.
119
-
120
- ```ts showLineNumbers
121
- import {mock} from 'jest-mock-extended';
122
- import {Logger} from './Logger';
123
- import {Foo} from './Foo';
124
-
125
- describe('Example', () => {
126
- let logger: Logger;
127
- let foo: Foo;
128
- let urlOpener: UrlOpener;
129
- let uut: Example;
130
-
131
- beforeEach(() => {
132
- logger = mock<Logger>();
133
- foo = mock<Foo>();
134
- urlOpener = mock<UrlOpener>();
135
- uut = new Example(logger, foo, urlOpener);
136
- });
137
- }
138
- ```
139
-
140
- ## Wrapping up
141
- While we didn't use any API from `Obsidian` in this refactor, this change was made possible due to how Obsidian influences the design of our code. Obsidian makes it easy to introduce classes to each other by passing them explicitly in through the constructor. This approach encourages us to split our code into smaller classes that are easier to test.
@@ -1,151 +0,0 @@
1
- // @ts-check
2
- // Note: type annotations allow type checking and IDEs autocompletion
3
-
4
- const lightCodeTheme = require('prism-react-renderer/themes/github');
5
- const darkCodeTheme = require('prism-react-renderer/themes/dracula');
6
-
7
- /** @type {import('@docusaurus/types').Config} */
8
- const config = {
9
- title: 'Obsidian',
10
- tagline: 'Dependency injection framework for React and React Native applications',
11
- url: 'https://wix-incubator.github.io',
12
- baseUrl: '/obsidian/',
13
- onBrokenLinks: 'throw',
14
- onBrokenMarkdownLinks: 'warn',
15
- favicon: 'img/favicon.ico',
16
-
17
- // GitHub pages deployment config.
18
- // If you aren't using GitHub pages, you don't need these.
19
- organizationName: 'Wix.com', // Usually your GitHub org/user name.
20
- projectName: 'obsidian', // Usually your repo name.
21
-
22
- // Even if you don't use internalization, you can use this field to set useful
23
- // metadata like html lang. For example, if your site is Chinese, you may want
24
- // to replace "en" with "zh-Hans".
25
- i18n: {
26
- defaultLocale: 'en',
27
- locales: ['en'],
28
- },
29
-
30
- presets: [
31
- [
32
- 'classic',
33
- /** @type {import('@docusaurus/preset-classic').Options} */
34
- ({
35
- docs: {
36
- sidebarPath: require.resolve('./sidebars.js'),
37
- // Please change this to your repo.
38
- // Remove this to remove the "edit this page" links.
39
- editUrl:
40
- 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
41
- },
42
- blog: {
43
- showReadingTime: true,
44
- // Please change this to your repo.
45
- // Remove this to remove the "edit this page" links.
46
- editUrl:
47
- 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
48
- },
49
- theme: {
50
- customCss: require.resolve('./src/css/custom.css'),
51
- },
52
- }),
53
- ],
54
- ],
55
-
56
- themeConfig:
57
- /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
58
- ({
59
- navbar: {
60
- title: 'Obsidian',
61
- logo: {
62
- alt: 'Obsidian Logo',
63
- src: 'img/logo.svg',
64
- },
65
- items: [
66
- {
67
- type: 'docSidebar',
68
- sidebarId: 'docs2',
69
- position: 'left',
70
- label: 'Documentation',
71
- },
72
- {
73
- type: 'docSidebar',
74
- sidebarId: 'guides',
75
- position: 'left',
76
- label: 'Guides',
77
- },
78
- {
79
- position: 'left',
80
- label: 'Playground',
81
- to: '/playground/',
82
- },
83
- // {to: '/blog', label: 'Blog', position: 'left'},
84
- {
85
- href: 'https://github.com/wix-incubator/obsidian',
86
- label: 'GitHub',
87
- position: 'right',
88
- },
89
- ],
90
- },
91
- footer: {
92
- style: 'dark',
93
- links: [
94
- {
95
- title: 'Docs',
96
- items: [
97
- {
98
- label: 'Tutorial',
99
- to: '/docs/documentation#the-2-steps-tutorial-for-injecting-dependencies-with-obsidian',
100
- },
101
- {
102
- label: 'Installation',
103
- to: '/docs/documentation/installation',
104
- },
105
- {
106
- label: 'API',
107
- to: '/docs/category/usage',
108
- }
109
- ],
110
- },
111
- {
112
- title: 'Community',
113
- items: [
114
- // {
115
- // label: 'Stack Overflow',
116
- // href: 'https://stackoverflow.com/questions/tagged/docusaurus',
117
- // },
118
- {
119
- label: 'Discord',
120
- href: 'https://discord.gg/2g5vhGQN',
121
- },
122
- // {
123
- // label: 'Twitter',
124
- // href: 'https://twitter.com/docusaurus',
125
- // },
126
- ],
127
- },
128
- {
129
- title: 'More',
130
- items: [
131
- // {
132
- // label: 'Blog',
133
- // to: '/blog',
134
- // },
135
- {
136
- label: 'GitHub',
137
- href: 'https://github.com/wix-incubator/obsidian',
138
- },
139
- ],
140
- },
141
- ],
142
- copyright: `Copyright © ${new Date().getFullYear()} Wix.com. Built with Docusaurus.`,
143
- },
144
- prism: {
145
- theme: lightCodeTheme,
146
- darkTheme: darkCodeTheme,
147
- },
148
- }),
149
- };
150
-
151
- module.exports = config;