The First one was using Vitest UI and the second one how to set up a CI with Vitest and GitHub Actions.
What is Vitest
I’m pretty sure you have already heard about vitest and vite in general. In case you have not, I would like to give you a short Update about what vitest is and why to use it.
Vite is a new frontend build tool with an insane performance. Vite has the power to immediately start up and do HMR in no time, even for large scale applications. It is developed by Evan You, the creator of Vue.js Anthony Fu, patak and 533 more amazing developers.
Vitest is a superfast unit-testing framework powered by Vite. It combines three things that could cause problems into one. Which means it is a test runner like jest, trantranspiles TypeScript, ESM etc. like Babel and bundles test dependencies like webpack. So instead of setting up three tools, you just need a single tool to test your application.
It is strongly focusing on providing the best possible developer experience. This includes incredible features like sharing the same config (vite.config.js) with your SUT. Which means that your tests run in the same Environment as your application does, which make a lot more sense to me. What made Vitest the testing tool of choice for my applications is the super Fast HMR. Vitest recognizes when changes were done. Vite immediately builds this changes, and all tests affected by those changes are executed in less than a minute.
For me, coming from jest, Vitests performance and simple configuration is a massive win and make TDD a lot more fun. It lets me freeze when thinking back to the dark days when I tried to set up Jest for TypeScript the first time. Vitest does this out of the box.
What are Parameterized (data-driven) tests
Parameterized tests originally comes from the Java world. More specifically, the feature was first introduced in JUnit 5 they also got adapted for Jest.
Parameterized tests gives you the opportunity to execute the same test with multiple different parameters. So you can write a test once and execute it for any given edge case or with multiple examples. Parameterized tests are data driven tests. The syntax in vitest is the same, as the parameterized test syntax in jest. Both work with .each, but vitest gone one step further than jest since they also introduced data driven test suits in Vitest!
Why should I use Parameterized tests?
- It saves you time
- You can test not only one example, you can test a whole range
- Less copy code
Instead of writing similar tests over and over again, you simply can pass different input and output values. This makes it way easier to test a whole range of Input and output.
Imagine you are going to test some special filter functionality.
With the traditional way, you would need one test to check if your code handles empty lists. Another one for checking duplicate values. Another one for checking no duplicates. Another one for the average case and maybe one with unhallowed values testing for error handling.
With Parameterized tests, you can use each time the same code and simply pass other parameters.
How to write Parameterized tests in Vitest?
In vitest you have two different but similar options. I will describe both of them.
Let's directly start with test.each
test.each in Vitest
As the name already suggests, test.each is used to execute a single test with the given Parameters.
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(%i, %i) -> %i', (a, b, expected) => {
expect(a + b).toBe(expected)
})
// this will return
// ✓ add(1, 1) -> 2
// ✓ add(1, 2) -> 3
// ✓ add(2, 1) -> 3
Using the example from the documentation, you can see that we are executing multiple tests with just writing one test and passing different Parameters to it.
describe.each in Vitest
Describe each follows the same principles as test.each with the difference that for describe.each you are executing complete test suites with multiple parameters.
describe.each([
{ a: 1, b: 1, expected: 2 },
{ a: 1, b: 2, expected: 3 },
{ a: 2, b: 1, expected: 3 },
])('describe object add($a, $b)', ({ a, b, expected }) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected)
})
test(`returned value not be greater than ${expected}`, () => {
expect(a + b).not.toBeGreaterThan(expected)
})
test(`returned value not be less than ${expected}`, () => {
expect(a + b).not.toBeLessThan(expected)
})
})
As you can see in the example from the documentation, we have one test suite defined with describe. We are feeding this whole test suite with multiple different parameters. Each test within this test suite gets executed three times with three different sets of Input and output values.
Difference between test and describe
As already mentioned above, the difference between test and describe is that for test, you are executing a single test and perform some actions with it.
With Describe you can group multiple tests together and execute those tests inside.
Within a describe block, you then have the full API of test available. That means you can nest test.each inside a describe.each. Or, you can run concurrent tests by nesting a test.each within a describe.concurrent block.
// All tests within this suite will be run in parallel
describe.concurrent("Reproduces race condition!", () => {
test.each([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])("add(%i, %i) -> %i", async (a, b, expected) => {
expect(a + b).toBe(expected);
});
});
So you can reproduce race conditions by only writing one test, my lazy programmers’ heart is now laughing.
Vitest Practical example of parameterized tests
Now, after we got down the theoretical part, let's round that up with a practical use case for parameterized tests.
What are we testing:
We are testing a function that determines if a given point lies inside a polygon or not.
This test was made to fix a bug where the function returned inverted results for negative coordinates. So we need to make sure that we are using a variety of different input values and validate that we always get the wished outcome. We do not want to not break the function for positive coordinates when we are fixing the negative ones.
Without Parameterized tests, we need to write for each case a new test. Let's see how it looks with Parameterized tests.
test.each([
[200, 200, 1000, 1000, 300, 300],
[0, 0, 1000, 1000, 300, 300],
[-100, -100, 1000, 1000, 100, 100],
[-100, 100, 1000, 1000, 1, 1],
[1000, -1000, 1000, 1000, 1, -1],
])(
"point in polygon for complete range start: %i / %i len: %i wth: %i point at: %i / %i",
(startX, startY, len, wth, x, y) => {
let corners: Corner[] = new PolyCreator()
.withWidth(wth)
.withLength(len)
.withRandomPoints()
.withStartCoordinates(startX, startY)
.create();
let polygons = sut.getPolygons();
// we have only 1 in memory
let isInPoly = polygons[0].pointInPoly(x, y);
expect(isInPoly).toBe(true);
}
);
As you can see above, we are executing five times the same test with different Coordinates, sizes, and click position. Without having duplicate code for those tests.
Below, you can see the output from vitest UI.
This would be the output for the failing tests:
This is so beautifully structured that I sometimes get happy when one of my test fails!
As you can see, you will also get all the information you need to resolve this issue from parameterized tests.
Conclusion
Using Parameterized tests could save you a lot of time and duplicate code. That makes your tests a lot more maintainable and saves you a lot of time.
The single thing I want to criticize is that it is hard to read the test results in command line.
But when you are using Vitest UI as described in this Article, everything is fine and amazingly readable.
So go ahead and try it out!
Happy testing,
Alex