Skip to Content
5. ExtrasUsing tests in your projects

Using tests in your projects

By Nael Shiab

Last updated on

Tests are widely used in programming to ensure that classes, functions, and methods behave as expected. When code is updated, tests guarantee that the changes are not breaking any of the work done previously.

But tests can also be very useful in data analysis and visualization projects. You can use them to make sure your calculations align with an independent reference. Or, once you have results, to ensure your code changes are not having unexpected impacts on your findings or visuals.

Personally, I use tests to increase my confidence in my work. I can double-check something once and write a test for it. Going forward, I’ll know for sure that this thing is guaranteed. And if the test fails, it means something very bad is happening and needs my attention!

In this lesson, we will code a simple function to calculate percentages and we will create tests to make sure it works as expected.

Want to know when new lessons are available? Subscribe to the newsletter ✉️ and give a ⭐ to the GitHub repository to keep me motivated! Click here to get in touch.

Setup

Create a new, empty folder, open it in VS Code, and create a file named main.ts inside it. Add this line and save the file.

main.ts
console.log("Hello!");

VS Code showing main.ts in the Explorer and console.log("Hello!") in the editor.

We’ll use Node’s built-in test runner. Once we’ve written a test, we’ll run it with node --test.

Writing the function to test

Create a helpers folder and put the getPercentage.ts file inside it with this code. It’s the function we want to test. It expects two numbers and returns the percentage as a string.

helpers/getPercentage.ts
export default function getPercentage( current: number, total: number, ): string { const percentage = (current / total) * 100; return `${percentage}%`; }

Update main.ts to use the function.

main.ts
import getPercentage from "./helpers/getPercentage.ts"; console.log(getPercentage(50, 100));

Save the file and run the command below in the VS Code terminal.

node main.ts

Everything looks fine! The function returns 50% as expected.

VS Code showing main.ts and getPercentage.ts side by side, with node main.ts printing 50% in the terminal.

Creating tests

Is our function really working as expected? Let’s check!

Creating a test is simple. We need two things.

  1. test from node:test lets us name a test and tell Node what code to run for it.
  2. assert from node:assert/strict lets us check the result. We’ll use assert.strictEqual to compare the result from the code we’re testing with the value we expect.

Both come with Node, so you don’t have to install anything else.

Now let’s create the test file. I like to mirror the structure of the rest of the code in my tests. Create a tests folder at the root of the project, put a helpers folder inside it, and create getPercentage.test.ts there. The .test.ts ending helps Node find it when you run the tests.

Add the following code to tests/helpers/getPercentage.test.ts.

tests/helpers/getPercentage.test.ts
import assert from "node:assert/strict"; import { test } from "node:test"; import getPercentage from "../../helpers/getPercentage.ts"; test("should return 50% for 50 out of 100", () => { const result = getPercentage(50, 100); assert.strictEqual(result, "50%"); });

Here’s what this code is doing.

  • Line 5 creates a new test with a description. You can write whatever you want there. Keep it simple and clear.
  • Lines 6 and 7 are the code the test runs. Line 6 calls getPercentage(50, 100) and stores the returned value in result. Line 7 uses assert.strictEqual to compare result with the expected string, "50%".

If result is exactly the string "50%", the test passes. Otherwise, it fails.

Running tests

Open the terminal in VS Code and run the command below. Node looks for test files ending in .test.ts or .test.js in your project, including in subfolders, and runs them.

node --test

You should see your test name with a check mark beside it, showing that it passed.

VS Code showing tests/helpers/getPercentage.test.ts and one passing test in the terminal.

💡

You might see type errors on lines 1 and 2 in VS Code. You can ignore them for this lesson. Both modules come with Node, and the test will still run.

Adding tests

Depending on your context, the test we wrote just above might be sufficient. For example, maybe you just wanted to double-check the result of one step in your data analysis and you want it to stay this way going forward.

Let’s add two more tests.

  • One checks that getPercentage(1, 3) returns "33.33%", with two decimal places.
  • The other checks that getPercentage(1, 0) throws an error because division by zero is undefined. We use assert.throws to check for the message Total must not be zero.

Write both tests against the current version of getPercentage. Update the test file with the code below.

tests/helpers/getPercentage.test.ts
import assert from "node:assert/strict"; import { test } from "node:test"; import getPercentage from "../../helpers/getPercentage.ts"; test("should return 50% for 50 out of 100", () => { const result = getPercentage(50, 100); assert.strictEqual(result, "50%"); }); test("should round 1 out of 3 to two decimal places", () => { const result = getPercentage(1, 3); assert.strictEqual(result, "33.33%"); }); test("should throw an error when the total is zero", () => { assert.throws(() => getPercentage(1, 0), { message: "Total must not be zero." }); });

Save the test file and run the tests again.

node --test

You’ll see the two new tests fail. The first test still passes.

The VS Code terminal shows one passing test and two failing tests for rounding and division by zero.

Scroll down in your terminal to see why each test failed. The rounding test expected "33.33%", but getPercentage(1, 3) returned "33.33333333333333%" with many more decimal places.

test at tests/helpers/getPercentage.test.ts:10:1 ✖ should round 1 out of 3 to two decimal places (0.44575ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: actual expected '33.33333333333333%' at TestContext.<anonymous> (file:///Users/nael/Desktop/test/tests/helpers/getPercentage.test.ts:12:12) at Test.runInAsyncScope (node:async_hooks:227:14) at Test.run (node:internal/test_runner/test:1402:25) at Test.processPendingSubtests (node:internal/test_runner/test:974:18) at Test.postRun (node:internal/test_runner/test:1542:19) at Test.run (node:internal/test_runner/test:1467:12) at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { generatedMessage: true, code: 'ERR_ASSERTION', actual: '33.33333333333333%', expected: '33.33%', operator: 'strictEqual', diff: 'simple' }

For the division-by-zero test, Missing expected exception tells us that getPercentage(1, 0) ran without throwing an error.

test at tests/helpers/getPercentage.test.ts:15:1 ✖ should throw an error when the total is zero (0.097542ms) AssertionError [ERR_ASSERTION]: Missing expected exception. at TestContext.<anonymous> (file:///Users/nael/Desktop/test/tests/helpers/getPercentage.test.ts:16:12) at Test.runInAsyncScope (node:async_hooks:227:14) at Test.run (node:internal/test_runner/test:1402:25) at Test.processPendingSubtests (node:internal/test_runner/test:974:18) at Test.postRun (node:internal/test_runner/test:1542:19) at Test.run (node:internal/test_runner/test:1467:12) at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { generatedMessage: false, code: 'ERR_ASSERTION', actual: undefined, expected: { message: 'Total must not be zero.' }, operator: 'throws', diff: 'simple' }

Let’s fix getPercentage so all our tests pass. 👷

Add rounding

Let’s start with rounding. Update helpers/getPercentage.ts as shown below to add an optional decimals parameter. If you leave it out, the function behaves as before. If you pass a number, toFixed rounds the percentage to that many decimal places.

helpers/getPercentage.ts
export default function getPercentage( current: number, total: number, decimals?: number, ): string { const percentage = (current / total) * 100; const formatted = decimals === undefined ? percentage.toString() : percentage.toFixed(decimals); return `${formatted}%`; }

In the rounding test, add 2 as the third argument to getPercentage to ask for two decimal places. The changed line is highlighted below.

tests/helpers/getPercentage.test.ts
import assert from "node:assert/strict"; import { test } from "node:test"; import getPercentage from "../../helpers/getPercentage.ts"; test("should return 50% for 50 out of 100", () => { const result = getPercentage(50, 100); assert.strictEqual(result, "50%"); }); test("should round 1 out of 3 to two decimal places", () => { const result = getPercentage(1, 3, 2); assert.strictEqual(result, "33.33%"); }); test("should throw an error when the total is zero", () => { assert.throws(() => getPercentage(1, 0), { message: "Total must not be zero." }); });

Save both files and run the tests again with the command below.

node --test

The first test still passes without the decimals argument. The second now passes too, so our rounding change worked! 🎉

The third test still fails. That’s expected because we haven’t handled division by zero yet.

VS Code shows the rounding change and the test results. The first two tests pass, and the division-by-zero test still fails.

Handle division by zero

To handle division by zero, add a check before dividing. Use an if statement to check whether total is zero. If it is, throw an error right away with the message the test expects.

Update getPercentage as shown below and save the file.

helpers/getPercentage.ts
export default function getPercentage( current: number, total: number, decimals?: number, ): string { if (total === 0) { throw new Error("Total must not be zero."); } const percentage = (current / total) * 100; const formatted = decimals === undefined ? percentage.toString() : percentage.toFixed(decimals); return `${formatted}%`; }

Run the tests once more with this command.

node --test

Now all our tests pass! 🎉

VS Code shows the check for a zero total and all three tests passing in the terminal.

Running and watching tests

You don’t need to run your whole test suite every time you change your code. To run a specific test file, pass its path to Node.

node --test tests/helpers/getPercentage.test.ts

To run a specific test, pass its description to --test-name-pattern.

node --test --test-name-pattern="should round 1 out of 3 to two decimal places"

Just like with node --watch main.ts, you can watch your tests with the command below. Node automatically reruns the affected tests when you save a change to a test file or one of its dependencies.

node --test --watch

Automated tests

You can always run tests locally, of course. You can even define an npm command to make it easier to run them.

But if you are using GitHub, you can set up automated tests in your projects. This is a common setup for open-source libraries, but you can use it for anything you want.

For example, I have hundreds of tests for the Simple Data Analysis library. You can explore them here .

I use GitHub Actions to run these tests automatically before publishing a new version of the library. The setup is simple. One small file  tells GitHub to run the tests in the cloud.

Testing web pages and data visualizations

In this lesson, we ran simple tests locally to check that our function returned the values we expected.

You can also test something more complex, like a web page. Does clicking a button or selecting an option display the right thing on the page? A library like Playwright  can run those tests in a browser and check what appears.

You can also test charts you export with Simple Data Analysis . Say your script saves a chart as an SVG. As you keep working on your analysis, you can compare the new SVG’s text with a version you’ve saved to catch unexpected changes to the chart, including changes in the data it shows.

If your script saves the chart as a PNG, you can compare it byte for byte with a reference file or use an image library to compare the pixels.

Just remember, pretty much anything is testable!

Conclusion

You don’t always need tests, but when you start working on a long-term or complex project, taking the time to write simple tests each time you are creating a new method or function can become time well invested.

You’ll be more confident in your code and, between type checking and tests, you’ll be able to avoid a lot of nasty bugs and errors! 🐞

And if you do have a bug to fix, adding a test to cover it will make sure it won’t ever happen again!

Happy testing! 👷

Enjoyed this? Want to know when new lessons are available? Subscribe to the newsletter ✉️ and give a ⭐ to the GitHub repository to keep me motivated! Get in touch if you have any questions.