Skip to content

Commit

Permalink
feat(tests): add object API
Browse files Browse the repository at this point in the history
Also add docs 😅
  • Loading branch information
Kent C. Dodds committed May 16, 2017
1 parent fe80771 commit df54a40
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 21 deletions.
232 changes: 230 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ Utilities for testing babel plugins

## The problem


You're writing a babel plugin and want to write tests for it.

## This solution


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

## Installation

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

## Usage

### import

```javascript
import pluginTester from 'babel-plugin-tester'
// or
const pluginTester = require('babel-plugin-tester')
```

### Invoke

```javascript
import yourPlugin from '../your-plugin'

pluginTester({
plugin: yourPlugin,
tests: [
/* your test objects */
],
})
```

### options

#### plugin

Your babel plugin. For example:

```javascript
pluginTester({
plugin: identifierReversePlugin,
tests: [
/* your test objects */
],
})

// normally you would import this from your plugin module
function identifierReversePlugin() {
return {
name: 'identifier reverse',
visitor: {
Identifier(idPath) {
idPath.node.name = idPath.node.name.split('').reverse().join('')
},
},
}
}
```

#### pluginName

This is used for the `describe` title as well as the test titles. If it
can be inferred from the `plugin`'s `name` then it will be and you don't need
to provide this option.

#### title

This can be used to specify a title for the describe block (rather than using
the `pluginName`).

#### fixtures

This is used in combination with the test object's `fixture` and `outputFixture`
options. This is used as the base directory with which to resolve relative
paths for those options.

Note: you really only need to specify this option if one of your test objects
uses `fixture` or `outputFixture` without absolute paths.

#### tests

You provide test objects as the `tests` option to `babel-plugin-tester`. You can
either provide the `tests` as an object of test objects or an array of test
objects.

If you provide the tests as an object, the key will be used as the title of the
test.

If you provide an array, the title will be derived from it's index and a
specified `title` property or the `pluginName`.

Read more about test objects below.

#### ...rest

The rest of the options you provide will be [`lodash.merge`][lodash-merge]d
with each test object. Read more about those next!

### Test Objects

A minimal test object can be:

1. A `string` representing code
2. An `object` with a `code` property

Here are the available properties if you provide an object:

#### code

The code that you want to run through your babel plugin. This must be provided
unless you provide a `fixture` instead. If there's no `output` or `outputFixture`
and `snapshot` is not `true`, then the assertion is that this code is unchanged
by the plugin.

#### title

If provided, this will be used instead of the `pluginName`. If you're using the
object API, then the `key` of this object will be the title (see example below).

#### output

If this is provided, the result of the plugin will be compared with this output
for the assertion. It will have any indentation stripped and will be trimmed as
a convenience for template literals.

#### fixture

If you'd rather put your `code` in a separate file, you can specify a filename
here. If it's an absolute path, that's the file that will be loaded, otherwise,
this will be `path.join`ed with the `fixtures` path.

#### outputFixture

If you'd rather put your `output` in a separate file, you can specify this
instead (works the same as `fixture`).

#### snapshot

If you'd prefer to take a snapshot of your output rather than compare it to
something you hard-code, then specify `snapshot: true`. This will take a
snapshot with both the source code and the output, making the snapshot easier
to understand.

## Examples

```javascript
import pluginTester from 'babel-plugin-tester'
import identifierReversePlugin from '../identifier-reverse-plugin'

pluginTester({
// required
plugin: identifierReversePlugin,

// unnecessary if it's returned with the plugin
pluginName: 'identifier reverse',

// defaults to the plugin name
title: 'describe block title',

// only necessary if you use fixture or outputFixture in your tests
fixtures: path.join(__dirname, '__fixtures__'),

// these will be `lodash.merge`d with the test objects
// below are the defaults:
babelOptions: {
parserOpts: {parser: recast.parse},
generatorOpts: {generator: recast.print, lineTerminator: '\n'},
babelrc: false,
},
snapshot: false, // use jest snapshots (only works with jest)

// tests as objects
tests: {
// the key is the title
// the value is the code that is unchanged (because `snapshot: false`)
// test title will be: `1. does not change code with no identifiers`
'does not change code with no identifiers': '"hello";',

// test title will be: `2. changes this code`
'changes this code': {
// input to the plugin
code: 'var hello = "hi";',
// expected output
output: 'var olleh = "hi";',
},
},

// tests as an array
tests: [
// should be unchanged by the plugin (because `snapshot: false`)
// test title will be: `1. identifier reverse`
'"hello";',
{
// test title will be: `2. identifier reverse`
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
},
{
// test title will be: `3. unchanged code`
title: 'unchanged code',
// because this is an absolute path, the `fixtures` above will not be
// used to resolve this path.
fixture: path.join(__dirname, 'some-path', 'unchanged.js'),
// no output, outputFixture, or snapshot, so the assertion will be that
// the plugin does not change this code.
},
{
// because these are not absolute paths, they will be joined with the
// `fixtures` path provided above
fixture: 'changed.js',
// because outputFixture is provided, the assertion will be that the
// plugin will change the contents of `changed.js` to the contents of
// `changed-output.js`
outputFixture: 'changed-output.js',
},
{
// as a convenience, this will have the indentation striped and it will
// be trimmed.
code: `
function sayHi(person) {
return 'Hello ' + person + '!'
}
`,
// this will take a jest snapshot. The snapshot will have both the
// source code and the transformed version to make the snapshot file
// easier to understand.
snapshot: true,
},
],
})
```

## Inspiration

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

- ESLint's [RuleTester][RuleTester]
- [@thejameskyle][@thejameskyle]'s [tweet][jamestweet]

## Other Solutions

Expand Down Expand Up @@ -98,3 +322,7 @@ MIT
[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/babel-plugin-tester.svg?style=social
[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
[all-contributors]: https://github.com/kentcdodds/all-contributors
[lodash.merge]: https://lodash.com/docs/4.17.4#merge
[RuleTester]: http://eslint.org/docs/developer-guide/working-with-rules#rule-unit-tests
[@thejameskyle]: https://github.com/thejameskyle
[jamestweet]: https://twitter.com/thejameskyle/status/864359438819262465
25 changes: 22 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as babel from 'babel-core'
import stripIndent from 'strip-indent'
import {oneLine} from 'common-tags'

export default pluginTester
module.exports = pluginTester

const fullDefaultConfig = {
parserOpts: {parser: recast.parse},
Expand All @@ -26,13 +26,14 @@ function pluginTester(
...rest
} = {},
) {
if (!tests || !tests.length) {
const testAsArray = toTestArray(tests)
if (!testAsArray.length) {
return
}
const testerConfig = merge({}, fullDefaultConfig, rest)

describe(describeBlockTitle, () => {
tests.forEach((testConfig, index) => {
testAsArray.forEach((testConfig, index) => {
if (!testConfig) {
return
}
Expand Down Expand Up @@ -88,6 +89,24 @@ function pluginTester(
})
}

function toTestArray(tests) {
tests = tests || [] // null/0/false are ok, so no default param
if (Array.isArray(tests)) {
return tests
}
return Object.keys(tests).reduce((testsArray, key) => {
let value = tests[key]
if (typeof value === 'string') {
value = {code: value}
}
testsArray.push({
title: key,
...value,
})
return testsArray
}, [])
}

function toTestConfig({testConfig, index, plugin, pluginName, fixtures}) {
if (typeof testConfig === 'string') {
testConfig = {code: stripIndent(testConfig).trim()}
Expand Down

0 comments on commit df54a40

Please sign in to comment.