@@ -36,149 +36,13 @@ We tried to make the right thing easy, but if you ignore these guidelines you ma
36
36
untestable application.
37
37
38
38
## Dependency Injection
39
- There are several ways in which you can get a hold of a dependency. You can:
40
- 1. Create it using the `new` operator.
41
- 2. Look for it in a well-known place, also known as a global singleton.
42
- 3. Ask a registry (also known as service registry) for it. (But how do you get a hold of
43
- the registry? Most likely by looking it up in a well known place. See #2.)
44
- 4. Expect it to be handed to you.
45
39
46
- Out of the four options in the list above, only the last one is testable. Let's look at why:
40
+ Angular comes with {@link di dependency injection} built-in, which makes testing components much
41
+ easier, because you can pass in a component's dependencies and stub or mock them as you wish.
47
42
48
- ### Using the `new` operator
49
-
50
- While there is nothing wrong with the `new` operator fundamentally, a problem arises when calling `new`
51
- on a constructor. This permanently binds the call site to the type. For example, let's say that we try to
52
- instantiate an `XHR` that will retrieve data from the server.
53
-
54
- ```js
55
- function MyClass() {
56
- this.doWork = function() {
57
- var xhr = new XHR();
58
- xhr.open(method, url, true);
59
- xhr.onreadystatechange = function() {...}
60
- xhr.send();
61
- }
62
- }
63
- ```
64
-
65
- A problem surfaces in tests when we would like to instantiate a `MockXHR` that would
66
- allow us to return fake data and simulate network failures. By calling `new XHR()` we are
67
- permanently bound to the actual XHR and there is no way to replace it. Yes, we could monkey
68
- patch, but that is a bad idea for many reasons which are outside the scope of this document.
69
-
70
- Here's an example of how the class above becomes hard to test when resorting to monkey patching:
71
-
72
- ```js
73
- var oldXHR = XHR;
74
- XHR = function MockXHR() {};
75
- var myClass = new MyClass();
76
- myClass.doWork();
77
- // assert that MockXHR got called with the right arguments
78
- XHR = oldXHR; // if you forget this bad things will happen
79
- ```
80
-
81
-
82
- ### Global look-up:
83
- Another way to approach the problem is to look for the service in a well-known location.
84
-
85
- ```js
86
- function MyClass() {
87
- this.doWork = function() {
88
- global.xhr({
89
- method:'...',
90
- url:'...',
91
- complete:function(response){ ... }
92
- })
93
- }
94
- }
95
- ```
96
-
97
- While no new dependency instance is created, it is fundamentally the same as `new` in
98
- that no way exists to intercept the call to `global.xhr` for testing purposes, other than
99
- through monkey patching. The basic issue for testing is that a global variable needs to be mutated in
100
- order to replace it with call to a mock method. For further explanation of why this is bad see: [Brittle Global
101
- State & Singletons](http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/)
102
-
103
- The class above is hard to test since we have to change the global state:
104
-
105
- ```js
106
- var oldXHR = global.xhr;
107
- global.xhr = function mockXHR() {};
108
- var myClass = new MyClass();
109
- myClass.doWork();
110
- // assert that mockXHR got called with the right arguments
111
- global.xhr = oldXHR; // if you forget this bad things will happen
112
- ```
113
-
114
-
115
- ### Service Registry:
116
-
117
- It may seem that this can be solved by having a registry of all the services and then
118
- having the tests replace the services as needed.
119
-
120
- ```js
121
- function MyClass() {
122
- var serviceRegistry = ????;
123
- this.doWork = function() {
124
- var xhr = serviceRegistry.get('xhr');
125
- xhr({
126
- method:'...',
127
- url:'...',
128
- complete:function(response){ ... }
129
- })
130
- }
131
- ```
132
-
133
- However, where does the serviceRegistry come from? If it is:
134
- * `new`-ed up, the test has no chance to reset the services for testing.
135
- * a global look-up then the service returned is global as well (but resetting is easier, since
136
- only one global variable exists to be reset).
137
-
138
- The class above is hard to test since we have to change the global state:
139
-
140
- ```js
141
- var oldServiceLocator = global.serviceLocator;
142
- global.serviceLocator.set('xhr', function mockXHR() {});
143
- var myClass = new MyClass();
144
- myClass.doWork();
145
- // assert that mockXHR got called with the right arguments
146
- global.serviceLocator = oldServiceLocator; // if you forget this bad things will happen
147
- ```
148
-
149
-
150
- ### Passing in Dependencies:
151
- Last, the dependency can be passed in.
152
-
153
- ```js
154
- function MyClass(xhr) {
155
- this.doWork = function() {
156
- xhr({
157
- method:'...',
158
- url:'...',
159
- complete:function(response){ ... }
160
- })
161
- }
162
- ```
163
-
164
- This is the preferred method since the code makes no assumptions about the origin of `xhr` and cares
165
- instead about whoever created the class responsible for passing it in. Since the creator of the
166
- class should be different code than the user of the class, it separates the responsibility of
167
- creation from the logic. This is dependency-injection in a nutshell.
168
-
169
- The class above is testable, since in the test we can write:
170
-
171
- ```js
172
- function xhrMock(args) {...}
173
- var myClass = new MyClass(xhrMock);
174
- myClass.doWork();
175
- // assert that xhrMock got called with the right arguments
176
- ```
177
-
178
- Notice that no global variables were harmed in the writing of this test.
179
-
180
- Angular comes with {@link di dependency injection} built-in, making the right thing
181
- easy to do, but you still need to do it if you wish to take advantage of the testability story.
43
+ Components that have their dependencies injected allow them to be easily mocked on a test by
44
+ test basis, without having to mess with any global variables that could inadvertently affect
45
+ another test.
182
46
183
47
## Additional tools for testing Angular applications
184
48
0 commit comments