Do you just use console.log
for everything?
When you run code locally, sometimes the easiest way to get the current value of something is console.log
to write to a terminal. This is easy. But sometimes, you need more insight into what a variable is really set to, and how it's set. Your editor's debugger is suitable for this.
Setting up VSCode's debugger to work with Javascript was once cumbersome. I would simply avoid it because I would waste as much time getting it to work as it would save me. Today, it's very simple. Now it is very easy for me to test breakpoints, and step through execution line by line. This is very handy in tricky situations where console.log
is a mess.
And it works very well with unit tests, trying to understand why a particular test just won't pass. In this example, we'll look at tests written with both mocha and jest.
Enable the debugger
In VSCode, open the command palette, and search for "JavaScript Debug Terminal"
Click that, and it will open a terminal that will automatically connect to the process with the debugger.
Set a breakpoint in your code:
The red dot indicates a breakpoint.
Then, run your code normally with npm start
or npm run test
.
That's it! It's that easy.
Now what
Execution should stop on your breakpoint, and controls like "Step over", "Step in", and "Step out" should work.
Step over - advance over this line and pause on the next line after
Step in - execute the current line, and if it calls a function, pause at the beginning of the function it steps into
Step out - execute the current line and the rest of the current function, pausing at the next line in whatever context called the current function.
And of course, it will always stop at breakpoints that you set explicitly.
In practice, stepping in/out/over doesn't always do what I expect, but it does work reasonably well, and it works great with await
lines these days.
Watch expressions are really handy:
Add a watch expression in the debug "watch" pane and you can see the current value of whatever expression you put in the box when execution pauses.
I thought it would be harder
There was a time when getting this to work would involve specific settings in your launch.json
or settings.json
file, and then you have to decide to commit those files, and decide whether the whole team should be locked into VSCode. This zero-config functionality is much better.
A caveat about node_modules
You might find that "Step in" will step all the way into 3rd party node modules - not your code - and Node.js internals - also not your code. Chances are you don't want to debug that. So, add this to a .vscode/settings.json
in your project.
{
"debug.javascript.terminalOptions": {
"skipFiles": [
"${workspaceFolder}/node_modules/**/*",
"<node_internals>/**"
]
}
}
So that's something you do need to commit and share, but it's not much! And you don't need it to get started.
Unit testing with mocha and jest
I said that this works for unit tests. There are caveats.
First, let's talk about mocha and jest
I won't dig into all the pros and cons or their alternatives, but these are the two most popular testing frameworks I know of for Node.js. They are very similar. There are differences.
Both mocha and jest:
Define tests with global
describe
andit
functions. These functions are almost the same between the two.Work with Typescript / ts-node
Configurable
Can run with parallelism
Can run in "watch" mode
mocha:
Single process by default. Can be configured for multiple processes, there are reasons you might not want to.
Formatters are available as plugins. It can generate output in lots of flavors.
mocks are not built in. You'll need a separate library.
typemoq
is handy.assertions are not built in. You'll need
chai
orshould
Code coverage is not built in. Use
nyc
instead.
jest:
Runs parallel tests by default
mocks are built in. But I also like to use
jest-mock-extended
.assertions are built in to a global
expect
functionCode coverage is built in with the
collectCoverage
option.
I tend to like jest better, but after writing this doc, I've softened on mocha and TypeMoq a bit.
Ok, what caveats
Tests will time out
Both mocha and jest have a default timeout for tests upon which they will fail. You probably want to take longer to debug. You will want to increase their configured timeout so you can take time to step through execution.
For this, I added a separate script to my package.json with a :debug
suffix. Here are examples for mocha and jest:
"test:mocha:debug": "nyc mocha --timeout 300000",
"test:jest:debug": "jest --testTimeout 300000"
Parallelism can bite you
If you normally have tests configured to run in parallel, which makes for faster CI builds, you might want to disable that for debugging. Jest and mocha will run each test suite (file) in a separate process, and there's no real guarantee that your debugger will be attached to the correct process.
I did experience some wonkiness when I wrote this up, but I couldn't reproduce the issue reliably enough to tell you how to make it happen. If it happens, then you probably want to try this:
"test:mocha:debug": "nyc mocha --timeout 300000 --jobs 1",
"test:jest:debug": "jest --testTimeout 300000 --maxWorkers 1"
Try it out
Run npm run test:mocha:debug
to see the output with mocha. Can you see the line that isn't covered and the failing test? Can you fix them?
Run npm run test:jest:debug
to see the output with jest. Can you see the line that isn't covered and the failing test? Can you fix them?
You're welcome to try out this example safely in a dev container.
Conclusion
You can use the debugger with zero config
This is great for when unit tests are hard to debug
There are caveats with unit tests around timeouts and parallelism
So, zero-config gets you started, but a little extra config gets you the rest of the way
Any additional tips and tricks? Let me know if there's a way to improve this.