Just a couple of years ago we rewrote our two main plugins, Nelio A/B Testing and Nelio Content, using TypeScript. We also rewrote our backend code in TypeScript. Why, you may ask? Pretty simple: we wanted our codebase to be more robust and prevent silly mistakes from happening, as TypeScript’s type checking capabilities would be able to catch type errors on compile time, before ever deploying new versions.
Later on, we also decided it’d be a good idea to add Zod in our tool-belt. This library made it possible to safely validate and parse unknown data given by our users and transforming it into the types and shapes we expected.
TypeScript and Zod are just the foundation of solid and reliable WordPress plugins. However, if we want to ensure the quality of our code in the long run, we need more. We need tests.
As you can imagine, tests are ways to check if the code works correctly and produces the results we expect. There’s different types of tests, depending on their scope:
- Unit Tests. These tests check small pieces of code (like functions) to make sure they do what they’re supposed to. Think of it as making sure each Lego block fits perfectly before building the whole structure.
- Integration Tests. They check if different parts of the program work together correctly. It’s like testing if all the Lego blocks, when put together, form the car or house you want.
- End-to-End (E2E) Tests. These tests check the whole application from start to finish, just like a real user would use it. It’s like making sure the Lego car can actually drive after it’s built.
- Regression Tests. These tests make sure that new changes to the code haven’t broken anything that was previously working. It’s like checking if adding a new Lego piece doesn’t mess up the car’s ability to drive.
- Manual Tests. Sometimes, people (not automated scripts) will manually use the program to make sure it works as expected. It’s like playing with the Lego set to see if everything works as it should.
In today’s post, I’d like to focus on unit tests: the first step towards an effective testing strategy.
Unit Tests: JavaScript vs TypeScript
Let’s start with an extremely simple example. Let’s say you defined the following JavaScript function:
function min( a, b ) {
return a < b ? a : b;
}Code language: JavaScript (javascript)which, given two numbers a and b, it’ll return the smallest. Some of the obvious tests are:
- If
ais smaller thanb, it should returna. So, for example,min(2,3)should return2. - If it’s the other way around, it should return
b. For instance,min(5,4)should return4. - If both numbers are equal, it doesn’t really matter:
min(10,10)should be10.
And then you might also want to think about floats, negative numbers, etc, and write those tests as well.
But what about the following scenarios:
- What should the function return if no arguments are provided? That is, what’s the expected result if we invoke
min()? - What if only one argument is provided (i.e.
min(3))? - What if one of the arguments or both arguments are not numbers? What should
min('hello', 'world')return?
In JavaScript, you simply don’t know the type of the arguments you’ll be given, and so you need to consider what happens when you get unexpected data. You can simply ignore it and hope everyone will use the function as expected, risking an unexpected error in runtime, or you can gracefully return a default value (like 0 in this case), you can throw different error messages… but, in any case, you have to deal with it somehow. And, ideally, your tests should reflect that decision.
In TypeScript, on the other hand, all these range of errors will simply not occur. Why? Because your function would look like this:
function min( a: number, b: number ): number {
return a < b ? a : b;
}Code language: TypeScript (typescript)and the type system would take care of all those weird edge cases. That is, if I try to call that function with anything other than two numeric arguments, TypeScript will complain and show a compilation error. This means that I can simply ignore all those checks from my tests, simply because I’m building them on top of a typed-checked codebase and a compiler has already taken care of them. Pretty cool, huh?
What If a TypeScript Function is Exposed in the Global Scope?
That’s a great question! Let’s say the previous function looks something like this instead:
window.min = ( a: number, b: number ): number =>
a < b ? a : b;Code language: TypeScript (typescript)which is a little bit silly, but bear with me. Now min is available in the global scope and, therefore, any other script in our page can invoke it. These other scripts are completely independent from my codebase, which means they might call this function as they please, be it with two numeric arguments or not.
So, in these circumstances, when you’re exposing a public JavaScript API that you’ve written in TypeScript, I recommend you type your arguments as unknown:
window.min = ( a: unknown, b: unknown ): number => {
if ( 'number' !== typeof a || 'number' !== typeof b ) {
throw new Error( 'Invalid type' );
}
return a < b ? a : b;
}Code language: TypeScript (typescript)because, well, you really don’t know what you’ll be given, do you? Then, all you gotta do is add the required type guards at the beginning to validate the input data and, if something’s wrong, throw an Error.

Nelio A/B Testing
By combining A/B tests made with Nelio, we doubled the revenue of webshops in just 12 months. I would recommend Nelio A/B testing to anyone with a WordPress site who wants to start A/B testing. It is definitely the best A/B testing plugin with the best price/quality for WordPress and WooCommerce.

Pieter Boonstra
So How to Create and Run Tests?
So let’s say we wanted to test the functions in our utils.ts module:
export const min = ( a: number, b: number ): number =>
a < b ? a : b;
export const max = ( a: number, b: number ): number =>
a > b ? a : b;
// etcCode language: TypeScript (typescript)All we gotta do is create a new file that will contain our tests, which we’ll name it utils.test.ts and write our tests there. In the TypeScript/JavaScript world, you’d usually use Jest. But, personally, I like to use Bun, which aims for compatibility with Jest (even though, at the time of writing this post, is not 100% compatible yet), because it’s blazingly fast, especially as your projects get bigger:
import { describe, expect, it } from 'bun:test';
import { min, max, ... } from './utils.ts';
describe( 'Export "min"', () => {
it( 'should return the minimum value', () => {
expect( min( 2, 3 ) ).toBe( 2 );
expect( min( 3, 2 ) ).toBe( 2 );
} );
it( 'should return either value if both are the same', () => {
expect( min( 2, 2 ) ).toBe( 2 );
} );
...
} );
describe( 'Export "max"', () => {
it ( 'should...', () => {
...
} );
} );Code language: TypeScript (typescript)Once you’ve written your tests, all you gotta do is run:
bun testCode language: Bash (bash)in your terminal and let Bun do its magic. It’s that simple!
Some Tips on Writing Your Tests
Finally, I’d like to end this post with a few tips that have helped me over the years, and I hope they’ll help you as well:
- Test one thing at a time. Each unit test should focus on a single behavior or functionality. This makes it easier to pinpoint the cause of a failure.
- Isolate your tests. Use setup and teardown functions (
beforeEachandafterEach) to prepare and clean up the environment for each test, ensuring isolation and independence of tests. - Minimize dependency mocking. Every time you need to mock a dependency, there’s a failure point in your test. If the dependency you are using no longer works like it used to, and your mock doesn’t reflect its new behavior, your test will fail to capture reality and will be useless.
- Keep tests small and fast. Unit tests should run quickly. Bun will be your friend here but, still, avoid slow operations like network requests or IO operations in your unit tests.
- Beware of side effects. Ensure that side effects are controlled or mocked out so that tests don’t affect each other.
- Indicate expected behavior. Test names should clearly describe what the test does. They should indicate the expected behavior, making it clear what the function is supposed to do.
- Use a consistent naming convention. Follow a consistent pattern, such as, for example, “should <expected behavior> [when <condition>]”.
- Group related tests. Use
describeblocks to group related tests. This makes it easier to understand the context of the tests. For instance, when there’s a bunch of tests that test some expected behaviors when a certain condition holds, use that “when <condition>” in thedescribeblock and then list all the tests inside. - Review and improve. Review your tests and their names from time to time to ensure they remain accurate and descriptive.
- Create tests on bugs. If you ever encounter a bug in your code or a user reports one, implement a test right away that captures said bug and then tweak the code until the test passes. This will guarantee you never introduce that bug into your code ever again.
I hope you liked this post and, if you did, let us know in the comment section below. See ya!
Featured Image by Cesar Carlevarino Aragon on Unsplash.




Leave a Reply