Skip to main content

Betterer v1.0.0 ☀️

· 8 min read

I'm stoked to announce v1.0.0 of Betterer!

I've been locked down in New Zealand for the last little while, and I've used some of that time to smash out what I think is a pretty compelling v1 release of a tool that I'm really excited about!

What is Betterer?

Betterer is a test runner that helps make incremental improvements to your code! It is based upon Jest's snapshot testing, but with a twist...

I'm sure many of us have been in situations where we've seen big changes we'd like to make, or new standards or design decisions that we'd like to encourage, but we just don't have the time to do it.

Usually one of two things happen:

  1. You start a long-lived branch that is awful to maintain and often impossible to merge. It ends up being a time sink ⏱

  2. You and your team make an agreement to make the improvement over time. It gets forgotten about and nothing gets better (in fact usually it gets worse!) 😕

I've seen this happen time and time and again! Sometimes it's introducing a new style rule to a codebase. Other times it's enabling stricter compilation, or decreasing the number of accessibility failures!

Betterer works in two stages. The first time it runs a test, it will take a snapshot of the current state. From that point on, whenever it runs it will compare against that snapshot. It will either throw an error (if the test got worse ❌), or update the snapshot (if the test got better ✅). That's pretty much it!

How does it work?

To get started, you can run the following from the root of your project:

npx @betterer/cli init

That will give you a brand new .betterer.ts config file which looks something like this:

// .betterer.ts

export default {
// Add tests here ☀️

From here, it's up to you to add some tests!

Let's imagine you're working with a codebase that uses Moment.js. You'd like to migrate away from it for performance reasons.

// src/subtract.js
import * as moment from 'moment';

const now = moment();

console.log(now.subtract(4, 'years'));

Let's also imagine that you're using ESLint in this codebase. One approach to remove Moment.js might be to use the no-restricted-imports ESLint rule, add the eslint-disable-next-line comment all over the place, and cross your fingers that people don't just add more... 🤔

Betterer gives us a better option! We can create a test for that specific rule:

// .betterer.ts
import { eslintBetterer } from '@betterer/eslint';

export default {
'no import from moment': eslintBetterer('./src/**/*.js', [
name: 'moment',
message: 'Please use "date-fns" instead.'

The first time we run the test with Betterer, it will look something like this:

Terminal output showing Betterer running and indicating that the "no import from moment" test has run for the first time

Betterer has now created a snapshot of the current state, stored by default in a .betterer.results file:

exports[`no import from moment`] = {
timestamp: 1589459511808,
value: `{
"src/subtract.js:566118541": [
[0, 0, 33, "\'moment\' import is restricted from being used. Please use \\"date-fns\\" instead.", "4035178381"]

The snapshot contains information about the current issues in the code.

The next time we run the test, it will look like this:

Terminal output showing Betterer running and indicating that the "no import from moment" test has run and stayed the same

Now, someone else on the team comes along and doesn't know about the new rule, and they add a new file that uses Moment.js:

// src/add.js
import * as moment from 'moment';

const now = moment();

console.log(now.add(4, 'years'));

When Betterer runs on their code, they get a nice big error:

Terminal output showing Betterer running and indicating that the "no import from moment" test has run and got worse

Even though a new issue has been introduced, the .betterer.results file doesn't change!

Our teammate reads the helpful error message from ESLint and they update their code to use date-fns...

// src/add.js
import { addYears } from 'date-fns';

console.log(addYears(, 4));

... and once again Betterer tells them that the result is the same:

Terminal output showing Betterer running and indicating that the "no import from moment" test has run and stayed the same

Our teammate has a bit of time on their hands, so they decide to fix up our usage of Moment.js as well! 🎉

This time when they run Betterer, everything is good:

Terminal output showing Betterer running and indicating that the "no import from moment" test has run and got better

There are now no remaining issues, so this test has met its goal. Since the existing issue has been resolved, it is removed from the snapshot in the .betterer.results file. This means we can move the rule from Betterer over to the normal ESLint configuration, so we don't reintroduce the issues again.

Pretty neat eh! That's an example of the built-in @betterer/eslint test, but there are other built-in tests too. And you can of course write your own tests! Check out the documentation for more details (still a WIP 🚧)!

What's in v1.0.0?

Everything I've mentioned so far has been working for a while! Over the last few months I've really solidified the implementation (basically a whole rewrite to be honest!):

  • Better error handling and error messages
  • Better issue comparison. It now understands file renames and issues that around within the same file
  • The ability to run tests on a single file via the JS API, with betterer.single
  • A whole bunch more tests!

But I've also added a few key features that are worthy of a 1.0.0 release! 🔥🔥🔥

Force Update (!)

First things first, you can now run Betterer with the --update option, and the snapshot will be updated even if it got worse! This is handy for when you need to ship something, even if it makes it temporarily worse:

betterer --update

And because this is shamelessly stolen from Jest, you can also use -u.

Watch mode (!!!)

This one is huge! All the changes that I made were building up to this. You can now run Betterer in watch mode and get feedback as you fix up issues:

Terminal output showing Betterer running in watch mode indicating that the "no import from moment" test has run on a specific file and got better

Same rules apply here, the snapshot will update whenever the test gets better!

There's a bunch of other cool things that could happen with watch mode (gamification much?), so I'm pumped that it's working! 🤩

VS Code extension (!!!!!)

Way to bury the lede! This is probably the coolest bit! Let's just say it was a build up.

Betterer runs entirely in its own world, so the usual ESLint or TypeScript extensions can't report the issues. But everyone loves seeing red squiggly lines in the code editor, so Betterer now has its very own VS Code Extension 🤯!

Initialise in a new project

You can run the betterer.init command in a project! It will generate config files and update your package.json with scripts and dependencies:

VS Code screen capture output showing Betterer being initialised in a project

See all issues in a file

And when you've got some tests setup, it will show you all the existing issues in a file, and when they were first created. And it will show you any new issues as you make them:

VS Code screen capture output showing Betterer highlighting issues in a project

I reckon that's pretty sweet!

So, what's next?

Well, you're going to try out Betterer and open lots of issues and help me make it better(er)! If you'd like to contribute...

  • There are definitely bugs to fix
  • The test coverage is pretty good, but there aren't any E2E tests for the VS Code extension yet
  • The logging and reporting implementation could use some work
  • There is so much documentation to be written

What a time to be alive! 🤓

In case you can't tell, I'm unreasonably excited about this and I really think this will help with large and legacy codebases. Please let me know what you think on the Twitters!