Skip to content

Commit df54a40

Browse files
author
Kent C. Dodds
committed
feat(tests): add object API
Also add docs 😅
1 parent fe80771 commit df54a40

File tree

3 files changed

+291
-21
lines changed

3 files changed

+291
-21
lines changed

README.md

+230-2
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ Utilities for testing babel plugins
2222

2323
## The problem
2424

25-
25+
You're writing a babel plugin and want to write tests for it.
2626

2727
## This solution
2828

29-
29+
This is a fairly simple abstraction to help you write tests for your babel
30+
plugin. It works with `jest` (my personal favorite) and most of it should also
31+
work with `mocha` and `jasmine`.
3032

3133
## Installation
3234

@@ -39,11 +41,233 @@ npm install --save-dev babel-plugin-tester
3941

4042
## Usage
4143

44+
### import
45+
46+
```javascript
47+
import pluginTester from 'babel-plugin-tester'
48+
// or
49+
const pluginTester = require('babel-plugin-tester')
50+
```
51+
52+
### Invoke
53+
54+
```javascript
55+
import yourPlugin from '../your-plugin'
56+
57+
pluginTester({
58+
plugin: yourPlugin,
59+
tests: [
60+
/* your test objects */
61+
],
62+
})
63+
```
64+
65+
### options
66+
67+
#### plugin
68+
69+
Your babel plugin. For example:
70+
71+
```javascript
72+
pluginTester({
73+
plugin: identifierReversePlugin,
74+
tests: [
75+
/* your test objects */
76+
],
77+
})
78+
79+
// normally you would import this from your plugin module
80+
function identifierReversePlugin() {
81+
return {
82+
name: 'identifier reverse',
83+
visitor: {
84+
Identifier(idPath) {
85+
idPath.node.name = idPath.node.name.split('').reverse().join('')
86+
},
87+
},
88+
}
89+
}
90+
```
91+
92+
#### pluginName
93+
94+
This is used for the `describe` title as well as the test titles. If it
95+
can be inferred from the `plugin`'s `name` then it will be and you don't need
96+
to provide this option.
97+
98+
#### title
99+
100+
This can be used to specify a title for the describe block (rather than using
101+
the `pluginName`).
102+
103+
#### fixtures
104+
105+
This is used in combination with the test object's `fixture` and `outputFixture`
106+
options. This is used as the base directory with which to resolve relative
107+
paths for those options.
108+
109+
Note: you really only need to specify this option if one of your test objects
110+
uses `fixture` or `outputFixture` without absolute paths.
111+
112+
#### tests
113+
114+
You provide test objects as the `tests` option to `babel-plugin-tester`. You can
115+
either provide the `tests` as an object of test objects or an array of test
116+
objects.
117+
118+
If you provide the tests as an object, the key will be used as the title of the
119+
test.
120+
121+
If you provide an array, the title will be derived from it's index and a
122+
specified `title` property or the `pluginName`.
123+
124+
Read more about test objects below.
125+
126+
#### ...rest
42127

128+
The rest of the options you provide will be [`lodash.merge`][lodash-merge]d
129+
with each test object. Read more about those next!
130+
131+
### Test Objects
132+
133+
A minimal test object can be:
134+
135+
1. A `string` representing code
136+
2. An `object` with a `code` property
137+
138+
Here are the available properties if you provide an object:
139+
140+
#### code
141+
142+
The code that you want to run through your babel plugin. This must be provided
143+
unless you provide a `fixture` instead. If there's no `output` or `outputFixture`
144+
and `snapshot` is not `true`, then the assertion is that this code is unchanged
145+
by the plugin.
146+
147+
#### title
148+
149+
If provided, this will be used instead of the `pluginName`. If you're using the
150+
object API, then the `key` of this object will be the title (see example below).
151+
152+
#### output
153+
154+
If this is provided, the result of the plugin will be compared with this output
155+
for the assertion. It will have any indentation stripped and will be trimmed as
156+
a convenience for template literals.
157+
158+
#### fixture
159+
160+
If you'd rather put your `code` in a separate file, you can specify a filename
161+
here. If it's an absolute path, that's the file that will be loaded, otherwise,
162+
this will be `path.join`ed with the `fixtures` path.
163+
164+
#### outputFixture
165+
166+
If you'd rather put your `output` in a separate file, you can specify this
167+
instead (works the same as `fixture`).
168+
169+
#### snapshot
170+
171+
If you'd prefer to take a snapshot of your output rather than compare it to
172+
something you hard-code, then specify `snapshot: true`. This will take a
173+
snapshot with both the source code and the output, making the snapshot easier
174+
to understand.
175+
176+
## Examples
177+
178+
```javascript
179+
import pluginTester from 'babel-plugin-tester'
180+
import identifierReversePlugin from '../identifier-reverse-plugin'
181+
182+
pluginTester({
183+
// required
184+
plugin: identifierReversePlugin,
185+
186+
// unnecessary if it's returned with the plugin
187+
pluginName: 'identifier reverse',
188+
189+
// defaults to the plugin name
190+
title: 'describe block title',
191+
192+
// only necessary if you use fixture or outputFixture in your tests
193+
fixtures: path.join(__dirname, '__fixtures__'),
194+
195+
// these will be `lodash.merge`d with the test objects
196+
// below are the defaults:
197+
babelOptions: {
198+
parserOpts: {parser: recast.parse},
199+
generatorOpts: {generator: recast.print, lineTerminator: '\n'},
200+
babelrc: false,
201+
},
202+
snapshot: false, // use jest snapshots (only works with jest)
203+
204+
// tests as objects
205+
tests: {
206+
// the key is the title
207+
// the value is the code that is unchanged (because `snapshot: false`)
208+
// test title will be: `1. does not change code with no identifiers`
209+
'does not change code with no identifiers': '"hello";',
210+
211+
// test title will be: `2. changes this code`
212+
'changes this code': {
213+
// input to the plugin
214+
code: 'var hello = "hi";',
215+
// expected output
216+
output: 'var olleh = "hi";',
217+
},
218+
},
219+
220+
// tests as an array
221+
tests: [
222+
// should be unchanged by the plugin (because `snapshot: false`)
223+
// test title will be: `1. identifier reverse`
224+
'"hello";',
225+
{
226+
// test title will be: `2. identifier reverse`
227+
code: 'var hello = "hi";',
228+
output: 'var olleh = "hi";',
229+
},
230+
{
231+
// test title will be: `3. unchanged code`
232+
title: 'unchanged code',
233+
// because this is an absolute path, the `fixtures` above will not be
234+
// used to resolve this path.
235+
fixture: path.join(__dirname, 'some-path', 'unchanged.js'),
236+
// no output, outputFixture, or snapshot, so the assertion will be that
237+
// the plugin does not change this code.
238+
},
239+
{
240+
// because these are not absolute paths, they will be joined with the
241+
// `fixtures` path provided above
242+
fixture: 'changed.js',
243+
// because outputFixture is provided, the assertion will be that the
244+
// plugin will change the contents of `changed.js` to the contents of
245+
// `changed-output.js`
246+
outputFixture: 'changed-output.js',
247+
},
248+
{
249+
// as a convenience, this will have the indentation striped and it will
250+
// be trimmed.
251+
code: `
252+
function sayHi(person) {
253+
return 'Hello ' + person + '!'
254+
}
255+
`,
256+
// this will take a jest snapshot. The snapshot will have both the
257+
// source code and the transformed version to make the snapshot file
258+
// easier to understand.
259+
snapshot: true,
260+
},
261+
],
262+
})
263+
```
43264

44265
## Inspiration
45266

267+
I've been thinking about this for a while. The API was inspired by:
46268

269+
- ESLint's [RuleTester][RuleTester]
270+
- [@thejameskyle][@thejameskyle]'s [tweet][jamestweet]
47271

48272
## Other Solutions
49273

@@ -98,3 +322,7 @@ MIT
98322
[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/babel-plugin-tester.svg?style=social
99323
[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
100324
[all-contributors]: https://github.com/kentcdodds/all-contributors
325+
[lodash.merge]: https://lodash.com/docs/4.17.4#merge
326+
[RuleTester]: http://eslint.org/docs/developer-guide/working-with-rules#rule-unit-tests
327+
[@thejameskyle]: https://github.com/thejameskyle
328+
[jamestweet]: https://twitter.com/thejameskyle/status/864359438819262465

src/index.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as babel from 'babel-core'
88
import stripIndent from 'strip-indent'
99
import {oneLine} from 'common-tags'
1010

11-
export default pluginTester
11+
module.exports = pluginTester
1212

1313
const fullDefaultConfig = {
1414
parserOpts: {parser: recast.parse},
@@ -26,13 +26,14 @@ function pluginTester(
2626
...rest
2727
} = {},
2828
) {
29-
if (!tests || !tests.length) {
29+
const testAsArray = toTestArray(tests)
30+
if (!testAsArray.length) {
3031
return
3132
}
3233
const testerConfig = merge({}, fullDefaultConfig, rest)
3334

3435
describe(describeBlockTitle, () => {
35-
tests.forEach((testConfig, index) => {
36+
testAsArray.forEach((testConfig, index) => {
3637
if (!testConfig) {
3738
return
3839
}
@@ -88,6 +89,24 @@ function pluginTester(
8889
})
8990
}
9091

92+
function toTestArray(tests) {
93+
tests = tests || [] // null/0/false are ok, so no default param
94+
if (Array.isArray(tests)) {
95+
return tests
96+
}
97+
return Object.keys(tests).reduce((testsArray, key) => {
98+
let value = tests[key]
99+
if (typeof value === 'string') {
100+
value = {code: value}
101+
}
102+
testsArray.push({
103+
title: key,
104+
...value,
105+
})
106+
return testsArray
107+
}, [])
108+
}
109+
91110
function toTestConfig({testConfig, index, plugin, pluginName, fixtures}) {
92111
if (typeof testConfig === 'string') {
93112
testConfig = {code: stripIndent(testConfig).trim()}

0 commit comments

Comments
 (0)