babel-plugin-tester
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
This module is distributed via npm which is bundled with node and
should be installed as one of your project's devDependencies
:
npm install --save-dev babel-plugin-tester
Usage
import
import pluginTester from 'babel-plugin-tester'
const pluginTester = require('babel-plugin-tester')
Invoke
import yourPlugin from '../your-plugin'
pluginTester({
plugin: yourPlugin,
tests: [
],
})
options
plugin
Your babel plugin. For example:
pluginTester({
plugin: identifierReversePlugin,
tests: [
],
})
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.
pluginOptions
This can be used to pass options into your plugin at transform time. This option
can be overwritten using the test object.
title
This can be used to specify a title for the describe block (rather than using
the pluginName
).
filename
Relative paths from the other options will be relative to this. Normally you'll
provide this as filename: __filename
. The only options
property affected by
this value is fixtures
. Test Object properties affected by this value are:
fixture
and outputFixture
. If those properties are not
absolute paths, then they will be path.join
ed with path.dirname
of the
filename
.
fixtures
This is a path to a directory with this format:
__fixtures__
├── first-test # test title will be: "first test"
│ ├── code.js # required
│ └── output.js # required
└── second-test
├── .babelrc # optional
├── code.js
└── output.js
With this you could make your test config like so:
pluginTester({
plugin,
fixtures: path.join(__dirname, '__fixtures__'),
})
And it would run two tests. One for each directory in __fixtures__
.
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
d
with each test object. Read more about those next!
Test Objects
A minimal test object can be:
- A
string
representing code - 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 filename
path.
outputFixture
If you'd rather put your output
in a separate file, you can specify this
instead (works the same as fixture
).
only
To run only this test. Useful while developing to help focus on a single test.
Can be used on multiple tests.
skip
To skip running this test. Useful for when you're working on a feature that is
not yet supported.
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.
error
If a particular test case should be throwing an error, you can that using one
of the following:
{
error: true,
error: 'should have this exact message',
error: /should pass this regex/,
error: SyntaxError,
error: err => {
if (err instanceof SyntaxError && /message/.test(err.message)) {
return true;
}
},
}
setup
If you need something set up before a particular test is run, you can do this
with setup
. This function will be run before the test runs. It can return
a function which will be treated as a teardown
function. It can also return
a promise. If that promise resolves to a function, that will be treated as a
teardown
function.
teardown
If you set up some state, it's quite possible you want to tear it down. You can
either define this as its own property, or you can return it from the setup
function. This can likewise return a promise if it's asynchronous.
formatResult
This is a function and if it's specified, it allows you to format the result
however you like. The use case for this originally was for testing codemods
and formatting their result with prettier-eslint
.
Examples
Full Example + Docs
import pluginTester from 'babel-plugin-tester'
import identifierReversePlugin from '../identifier-reverse-plugin'
pluginTester({
plugin: identifierReversePlugin,
pluginName: 'identifier reverse',
title: 'describe block title',
pluginOptions: {
optionA: true,
},
filename: __filename,
babelOptions: {
parserOpts: {},
generatorOpts: {},
babelrc: false,
},
snapshot: false,
tests: {
'does not change code with no identifiers': '"hello";',
'changes this code': {
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
},
},
tests: [
'"hello";',
{
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
},
{
title: 'unchanged code',
fixture: path.join(__dirname, 'some-path', 'unchanged.js'),
},
{
fixture: '__fixtures__/changed.js',
outputFixture: '__fixtures__/changed-output.js',
},
{
code: `
function sayHi(person) {
return 'Hello ' + person + '!'
}
`,
snapshot: true,
},
{
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
pluginOptions: {
optionA: false,
},
},
{
title: 'unchanged code',
setup() {
return function teardown() {
}
},
teardown() {
},
},
],
})
Simple Example
import pluginTester from 'babel-plugin-tester'
import identifierReversePlugin from '../identifier-reverse-plugin'
pluginTester({
plugin: identifierReversePlugin,
snapshot: true,
tests: [
{code: '"hello";', snapshot: false},
{
code: 'var hello = "hi";',
output: 'var olleh = "hi";',
},
`
function sayHi(person) {
return 'Hello ' + person + '!'
}
console.log(sayHi('Jenny'))
`,
],
})
Inspiration
I've been thinking about this for a while. The API was inspired by:
Other Solutions
I'm not aware of any, if you are please make a pull request and add it
here!
Contributors
Thanks goes to these people (emoji key):
This project follows the all-contributors specification.
Contributions of any kind welcome!
LICENSE
MIT