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:
You start a long-lived branch that is awful to maintain and often impossible to merge. It ends up being a time sink ⏱
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', [
'no-restricted-imports',
[
'error',
{
name: 'moment',
message: 'Please use "date-fns" instead.'
}
]
])
};
The first time we run the test with Betterer, it will look something like this:
Betterer has now created a snapshot of the current state, stored by default in a .betterer.results
file:
// BETTERER RESULTS V1.
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:
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:
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(Date.now(), 4));
... and once again Betterer tells them that the result is 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:
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:
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:
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:
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!