Unit tests are very useful for checking how our app is working.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.
Use Realistic Input Data
We should use realistic input data in our tests so that we know what we’re testing with.
To make generate fake data easily, we can use the Faker package.
It can generate names, username, company names, credit card numbers, and more.
For example, we can write:
it("should add product", async () => {
const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
expect(addProductResult).to.be.true;
});
We have a test to add a product with realistic names and ID number so that we can understand the result.
Test Many Input Combinations
We should test many input combinations.
This way, we won’t only choose cases that we know will make our test pass.
We can make the values random.
And we can also several permutations of some data in our test.
For example, with the fast-check library, we can create random combinations of data for our test:
import fc from "fast-check";
describe("Product service", () => {
describe("add new product", () => {
it("add product with various combinations successfully", () =>
fc.assert(
fc.property(fc.integer(), fc.string(), (id, name) => {
expect(addNewProduct(id, name).status).toEqual("success");
})
));
});
});
We called addnewProduct
with random values of id
and name
and check if the returned status is 'success'
.
This way, we can’t rig our test to pass with only some values.
Use Only Short and Inline snapshots
We should use short and inline snapshots in or tests to let us create UI tests that fast to run.
If they can be added inline, then we know that they’ll be small.
If it’s so big that it can only be stored in an external file, then it’ll probably slow down our tests too much.
For example, we can write:
it("when we go to example.com, a menu is displayed", () => {
const receivedPage = renderer
.create(<DisplayPage page="http://www.example.com">Example</DisplayPage>)
.toJSON();
const menu = receivedPage.content.menu;
expect(menu).toMatchInlineSnapshot(`<ul>
<li>Home</li>
<li>Profile</li>
<li>Logout</li>
</ul>`);
});
We render the DisplayPage
component and then check against the snapshot that we created inline.
Avoid Global Test Fixtures and Seeds
We should create our data per test and clean them out after each test.
This way, we always get a clean environment for our tests.
And the tests won’t depend on each other.
This is important since we’ll run into problems when tests depend on each other.
If performance becomes a concern with creating data for each test, then we’ve to simplify the data.
So if we test with database interaction, we got to remove all the data after each test.
Expect Errors
If we expect errors, then we document the errors that are thrown into our app.
In most JavaScript test frameworks, we have something like:
expect(method).toThrow())
to check if method
throws something after we do something.
Sweeping errors under the rug just make them hard to find.
And it still doesn’t do what we expect.
So we can write something like:
it("when no data provided, it throws error 400", async () => {
await expect(addUser({}))
.to.eventually.throw(AppError)
.with.property("code", "invalid input");
});
Conclusion
We should use realistic data for tests.
Also, we use inline snapshots to make our tests faster.
We also should test with many kinds of inputs.