Using tests in your projects
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.
Setup
We will use Deno and VS Code in this lesson. Let’s create a simple file structure.
Create a new folder helpers
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.
export default function getPercentage(
current: number,
total: number,
): string {
const percentage = (current / total) * 100;
return `${percentage}%`;
}
Now create a main.ts
file and use the function.
import getPercentage from "./helpers/getPercentage.ts";
console.log(getPercentage(50, 100));
From here, you can execute the script with deno main.ts
.
Everything looks fine! The function returns 50%
as expected.
Creating tests
Is our function really working as expected? Let’s check!
First, install the @std/assert standard library with: deno add jsr:@std/assert
. We will use it to compare the returned values of our function with what we expect them to be.
Now, let’s create the test file. One way to keep tests organized in a project is to keep the same file structure for the tests as for the rest of the code.
Here, we could create a new folder tests
with a new file getPercentage.test.ts
in it. Note that this file is a regular TypeScript file since it ends with .ts
. But because we added .test
just before the extension, Deno now knows this is a test.
Here’s what the code below does:
Deno.test
creates a new test.- The first parameter is a description of the test.
- The second parameter is a function. We run what we want in it (here
getPercentage
) and we check that the result is what we expect withassertEquals
. - If the result is what is expected, the test will pass. Otherwise, it will fail!
import { assertEquals } from "@std/assert";
import getPercentage from "../helpers/getPercentage.ts";
Deno.test("should return 50% for 50 out of 100", () => {
const result = getPercentage(50, 100);
assertEquals(result, "50%");
});
Running tests
To run tests with Deno, you can simply run deno test
. Deno will look for all .test.ts
files in the project and run them.
You can also run just one test file if you want. For example, here it would be deno test tests/getPercentage.test.ts
.
If you need to fetch something over the internet, or to read/write files, remember to pass the appropriate permissions. To authorize everything, you can use this command: deno test -A
.
However, it’s not convenient to rerun this command every time you update your code. Like we saw in other lessons, it would be better to watch files and rerun the tests automatically.
To do so, you can use the watch option: deno test --watch
. Run this command and keep it running for the rest of the lesson.
There are a lot of other options as explained in the documentation. Another one that I use a lot is --fail-fast
, which stops running all the tests as soon as one fails.
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.
But for the sake of this lesson, let’s add more tests here. Let’s test edge cases.
For example, what is supposed to happen when the first number is 0
? And when the second is 0
? Maybe we want our function to return 0%
in both cases.
Copy and paste the code below. When you save the modified file, the tests will be automatically rerun.
import { assertEquals } from "@std/assert";
import getPercentage from "../helpers/getPercentage.ts";
Deno.test("should return 50% for 50 out of 100", () => {
const result = getPercentage(50, 100);
assertEquals(result, "50%");
});
Deno.test("should return 0% for 0 out of 100", () => {
const result = getPercentage(0, 100);
assertEquals(result, "0%");
});
Deno.test("should return 0% for 1 out of 0", () => {
const result = getPercentage(1, 0);
assertEquals(result, "0%");
});
We can see that the second test passed but the third one failed!
When the total
is 0
, our function is returning Infinity
instead of what we want, which is 0%
.
Let’s update our getPercentage.ts
to fix that. Copy and paste the code below. When you save the file, the tests will rerun automatically.
export default function getPercentage(
current: number,
total: number,
): string {
if (total === 0) {
return "0%";
} else {
const percentage = (current / total) * 100;
return `${percentage}%`;
}
}
And now, we can see that the third test passed.
We also know that the previous behavior is still there because the first two tests passed too!
Automated tests
You can always run tests locally, of course. You can even make a task 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 in place for the Simple Data Analysis library. You can explore them here.
I use GitHub Actions (check the lesson about it) to trigger them automatically when a pull request is made to merge new code to the main branch.
If the tests pass, the merge is allowed and the new version of the library can be published. But if one or more tests fail, it means there is a breaking change or unexpected behavior, and the PR is rejected. More work is needed. You can check the workflow here.
It’s just a few lines of code, but it guarantees that the library is working as expected. And it has saved me many times from publishing a new broken version of the library! 😇
Testing dataviz
Tests are easy to set up when your code returns something like a string, number, date, array, or object.
But you can also use tests to compare web page elements, like charts, and more, with Playwright. We used Playwright in the web scraping lesson, but it was actually created for tests!
The docs are well done. There is even a section about automatically comparing visuals with screenshots.
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! 👷