Skip to content

Commit d908c5a

Browse files
authored
Merge pull request #220 from testdouble/third-party-module-replacement
Add support for third party modules
2 parents 20da70a + e91ae44 commit d908c5a

File tree

13 files changed

+182
-46
lines changed

13 files changed

+182
-46
lines changed

docs/7-replacing-dependencies.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,43 @@ late to have their intended effect).
103103

104104
### Aside: third-party modules
105105

106-
If you're curious why testdouble.js doesn't support replacing third-party
107-
modules, you can see our commentary on why we "[don't mock what we don't
108-
own](B-frequently-asked-questions.md#why-doesnt-tdreplace-work-with-external-commonjs-modules)".
106+
testdouble.js can also replace third-party npm modules. For instance, if you
107+
depend on the module [is-number](https://npmjs.org/package/is-number), you can,
108+
in your test:
109+
110+
```js
111+
var isNumber = td.replace('is-number')
112+
var numbersOnly = require('./numbers-only')
113+
td.when(isNumber('a string')).thenReturn(true) // tee-hee, this is silly
114+
115+
var result = numbersOnly('a string')
116+
117+
assert.equal(result, true)
118+
```
119+
120+
Should pass for a subject:
121+
122+
```js
123+
var isNumber = require('is-number')
124+
125+
module.exports = function (thing) {
126+
if (!isNumber(thing)) {
127+
throw new Error('numbers only!')
128+
}
129+
return true
130+
}
131+
```
132+
133+
Even though testdouble.js does support replacing third-party npm modules, it is
134+
not recommended unless you own the module! Typically, when practicing the sort
135+
of outside-in test-driven development that testdouble.js is designed to
136+
facilitate, you should keep third-party dependencies at arms-length by only
137+
[mocking what you
138+
own](http://github.com/testdouble/contributing-tests/wiki/Don%27t-mock-what-you-don%27t-own).
139+
But if you're managing lots of internal modules and they're all in a consistent
140+
style such that the line between first-party & third-party code is blurred, then
141+
`td.replace` has you covered and should be able to replace third-party modules
142+
or npm packages just like it can for local paths.
109143

110144
## Browser
111145

docs/B-frequently-asked-questions.md

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,6 @@ Whenever someone asks a particularly salient question about testdouble.js, we'll
44
log it here for the benefit of anyone reading up on how to make the most use of
55
the library.
66

7-
## Why doesn't `td.replace()` work with external CommonJS modules?
8-
9-
[Jörn Zaefferer asked](https://github.com/testdouble/testdouble.js/issues/51)
10-
whether testdouble.js would support replacing third-party CommonJS modules in
11-
Node.js with the [td.replace()](7-replacing-dependencies.md) feature.
12-
13-
The short answer is "no, testdouble.js does not plan to support this".
14-
15-
The longer answer is that when we practice TDD with test doubles, we "don't mock
16-
what we don't own". The reason for using mocks in our practice is to improve the
17-
richness of design feedback by making awkward interactions with dependencies
18-
painful; the appropriate response to that pain is to make that dependency's API
19-
better.
20-
21-
With that being the purpose of using test doubles, replacing a third-party API
22-
will often just lead to *useless pain*, because the author isn't in an immediate
23-
position to improve the third party API.
24-
25-
Rather, our use of 3rd party dependencies (insofar as how our unit tests interact
26-
with them) typically break down into two categories:
27-
28-
* Utility functions (e.g. `lodash`) — we call through to the real utility
29-
function from our unit test so long as they don't add additional side effects or
30-
break the purity of the function; I use these much more in unit tests of pure
31-
functions, which themselves typically don't require any dependencies (and
32-
therefore test doubles) at all
33-
* Integration functions (e.g. `request`) — we create adapters/wrappers of these
34-
dependencies that delegate to the third-party functions and then we replace those
35-
adapters/wrappers in our tests. That way, any custom branching, configuration,
36-
or API de-awkward-ification we apply to that dependency is in the wrapper and
37-
effectively centralized in a single place in the app. Not only is this a great
38-
way to prevent a third party module from seeping throughout the codebase, but it
39-
can provide a template for how to later replace that area of functionality with
40-
a different 3rd party dependency. It can even be a good first step towards
41-
introducing an adapter pattern to deal with an option of multiple 3rd party
42-
dependencies.
43-
44-
As a result, we aren't planning to go out of our way to support replacing modules
45-
with quibble / td.js
46-
477
## Why shouldn't I call both td.when and td.verify for a single interaction with a test double?
488

499
It's a common mistake to call `td.verify` for an invocation that's already been

examples/node/lib/elastic-thing.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
var elasticsearch = require('elasticsearch')
2+
3+
module.exports = function () {
4+
return elasticsearch.Client()
5+
}

examples/node/lib/numbers-only.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
var isNumber = require('is-number')
2+
3+
module.exports = function (thing) {
4+
if (!isNumber(thing)) {
5+
throw new Error('numbers only!')
6+
}
7+
return true
8+
}

examples/node/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
},
88
"devDependencies": {
99
"chai": "^3.3.0",
10+
"elasticsearch": "^12.1.3",
11+
"is-number": "^3.0.0",
1012
"mocha": "^2.3.3",
1113
"testdouble": "*"
1214
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
describe('elastic-thing', function () {
2+
it('is quite elastic', function () {
3+
var elasticsearch = td.replace('elasticsearch')
4+
var subject = require('../../lib/elastic-thing')
5+
td.when(elasticsearch.Client()).thenReturn('pants')
6+
7+
var result = subject()
8+
9+
expect(result).to.eq('pants')
10+
})
11+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
describe('numbers-only', function () {
2+
it('replaces modules ok', function () {
3+
var isNumber = td.replace('is-number')
4+
var numbersOnly = require('../../lib/numbers-only')
5+
td.when(isNumber('a string')).thenReturn(true) // tee-hee, this is silly
6+
7+
var result = numbersOnly('a string')
8+
9+
expect(result).to.eq(true)
10+
})
11+
})

examples/node/yarn.lock

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22
# yarn lockfile v1
33

44

5+
ansi-regex@^2.0.0:
6+
version "2.1.1"
7+
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
8+
9+
ansi-styles@^2.2.1:
10+
version "2.2.1"
11+
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
12+
13+
asap@~2.0.3:
14+
version "2.0.5"
15+
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f"
16+
517
assertion-error@^1.0.1:
618
version "1.0.2"
719
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
@@ -14,6 +26,16 @@ chai@^3.3.0:
1426
deep-eql "^0.1.3"
1527
type-detect "^1.0.0"
1628

29+
chalk@^1.0.0:
30+
version "1.1.3"
31+
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
32+
dependencies:
33+
ansi-styles "^2.2.1"
34+
escape-string-regexp "^1.0.2"
35+
has-ansi "^2.0.0"
36+
strip-ansi "^3.0.0"
37+
supports-color "^2.0.0"
38+
1739
1840
version "0.6.1"
1941
resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06"
@@ -38,10 +60,23 @@ [email protected]:
3860
version "1.4.0"
3961
resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf"
4062

41-
63+
elasticsearch@^12.1.3:
64+
version "12.1.3"
65+
resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-12.1.3.tgz#5108e67ae5d83e5e7f30d3d294fd7017df0e3771"
66+
dependencies:
67+
chalk "^1.0.0"
68+
forever-agent "^0.6.0"
69+
lodash "^4.12.0"
70+
promise "^7.1.1"
71+
72+
[email protected], escape-string-regexp@^1.0.2:
4273
version "1.0.2"
4374
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1"
4475

76+
forever-agent@^0.6.0:
77+
version "0.6.1"
78+
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
79+
4580
4681
version "3.2.11"
4782
resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d"
@@ -53,10 +88,26 @@ [email protected]:
5388
version "1.9.2"
5489
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
5590

91+
has-ansi@^2.0.0:
92+
version "2.0.0"
93+
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
94+
dependencies:
95+
ansi-regex "^2.0.0"
96+
5697
inherits@2:
5798
version "2.0.3"
5899
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
59100

101+
is-buffer@^1.0.2:
102+
version "1.1.5"
103+
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
104+
105+
is-number@^3.0.0:
106+
version "3.0.0"
107+
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
108+
dependencies:
109+
kind-of "^3.0.2"
110+
60111
is-plain-obj@^1.0.0:
61112
version "1.1.0"
62113
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -72,7 +123,13 @@ [email protected]:
72123
commander "0.6.1"
73124
mkdirp "0.3.0"
74125

75-
lodash@^4.15.0, lodash@^4.17.2:
126+
kind-of@^3.0.2:
127+
version "3.1.0"
128+
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47"
129+
dependencies:
130+
is-buffer "^1.0.2"
131+
132+
lodash@^4.12.0, lodash@^4.15.0, lodash@^4.17.2:
76133
version "4.17.4"
77134
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
78135

@@ -120,6 +177,12 @@ [email protected]:
120177
version "0.7.1"
121178
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
122179

180+
promise@^7.1.1:
181+
version "7.1.1"
182+
resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf"
183+
dependencies:
184+
asap "~2.0.3"
185+
123186
quibble@^0.4.0:
124187
version "0.4.0"
125188
resolved "https://registry.yarnpkg.com/quibble/-/quibble-0.4.0.tgz#a1535c4a80b3d3617d23c5d770f1ec2c7b5523a1"
@@ -137,10 +200,20 @@ stringify-object@^2.4.0:
137200
is-plain-obj "^1.0.0"
138201
is-regexp "^1.0.0"
139202

203+
strip-ansi@^3.0.0:
204+
version "3.0.1"
205+
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
206+
dependencies:
207+
ansi-regex "^2.0.0"
208+
140209
141210
version "1.2.0"
142211
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e"
143212

213+
supports-color@^2.0.0:
214+
version "2.0.0"
215+
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
216+
144217
testdouble@*:
145218
version "1.11.2"
146219
resolved "https://registry.yarnpkg.com/testdouble/-/testdouble-1.11.2.tgz#8b6a2e418ad1da9991e479203e37528dfd81d557"

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"dependencies": {
6262
"lodash": "^4.15.0",
6363
"quibble": "^0.4.1",
64+
"resolve": "^1.3.2",
6465
"stringify-object-es5": "^2.5.0"
6566
},
6667
"devDependencies": {
@@ -73,6 +74,7 @@
7374
"coffee-script": "^1.10.0",
7475
"coffeeify": "^2.1.0",
7576
"headerify": "^1.0.1",
77+
"is-number": "^3.0.0",
7678
"mocha": "^3.2.0",
7779
"mocha-given": "^0.1.3",
7880
"nyc": "^10.1.2",

src/replace/module.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import imitate from './imitate'
22
import quibble from 'quibble'
3+
import resolve from 'resolve'
34

45
quibble.ignoreCallsFromThisFile()
56

67
export default function (path, stub) {
78
if (arguments.length > 1) { return quibble(path, stub) }
8-
const realThing = require(quibble.absolutify(path))
9+
const realThing = requireAt(path)
910
const fakeThing = imitate(realThing, path)
1011
quibble(path, fakeThing)
1112
return fakeThing
1213
}
14+
15+
var requireAt = (path) => {
16+
try {
17+
// 1. Try just following quibble's inferred path
18+
return require(quibble.absolutify(path))
19+
} catch (e) {
20+
// 2. Try including npm packages
21+
return require(resolve.sync(path, { basedir: process.cwd() }))
22+
}
23+
}

0 commit comments

Comments
 (0)