What is Unit Testing?
Unit testing is a software development practice that involves isolating individual units of code (functions, classes, modules) and verifying their correctness under various conditions. It ensures that each unit behaves as expected and produces the intended output for a given set of inputs.
Benefits of Unit Testing:
Improved Code Quality: Catches errors early in the development process, leading to more robust and reliable software.
Increased Confidence in Changes: Makes developers more confident to modify code without introducing regressions since unit tests act as a safety net.
Better Maintainability: Well-written unit tests document how code works, improving code comprehension for future maintainers.
Let's consider a simple function in TypeScript that calculates the area of a rectangle:
// area.ts
export function calculateArea(width: number, height: number): number {
return width * height;
}
import { expect, describe, it } from 'vitest';
import { calculateArea } from './area';
describe('calculateArea function', () => {
it('should calculate the area of a rectangle correctly', () => {
const width = 5;
const height = 4;
const expectedArea = 20;
const actualArea = calculateArea(width, height);
expect(actualArea).toEqual(expectedArea);
});
it('should return 0 for zero width or height', () => {
const testCases = [
{ width: 0, height: 5, expectedArea: 0 },
{ width: 5, height: 0, expectedArea: 0 },
];
for (const testCase of testCases) {
const { width, height, expectedArea } = testCase;
const actualArea = calculateArea(width, height);
expect(actualArea).toEqual(expectedArea);
}
});
});
Explanation:
We import
expect
from Vitest for assertions.We import
calculateArea
from our area.ts file.We use
describe
to create a test suite forcalculateArea
.-
Within the
describe
block, we define two test cases usingit
:-
The first test verifies if the function calculates the area correctly for non-zero dimensions.
- We define
width
,height
, andexpectedArea
. - We call
calculateArea
with the defined values. - We use
expect
to assert that the actual area (actualArea
) matches the expected area.
- We define
-
The second test covers scenarios with zero width or height.
- We create an array of test cases (
testCases
) with different input values. - We iterate through each test case using a
for
loop. - For each case, we extract
width
,height
, andexpectedArea
. - We call
calculateArea
with these values and assert the result usingexpect
.
- We create an array of test cases (
-
Elements of Unit testing:
The elements that make up a unit test in Vitest (or any other unit testing framework) can be broken down into three main parts:
-
Test Runner and Assertions:
- Test Runner: This is the core functionality that executes your test cases and provides the framework for running them. Vitest leverages the power of Vite for fast test execution.
-
Assertions: These are statements that verify the expected outcome of your tests. Vitest offers built-in assertions (like
expect
) or allows using libraries like Chai for more advanced assertions.
-
Test Description:
-
describe
andit
blocks: These functions from Vitest (similar to other frameworks) structure your tests.-
describe
defines a test suite that groups related tests for a specific functionality. - Within
describe
, individual test cases are defined usingit
blocks. Eachit
block describes a specific scenario you want to test.
-
-
-
Test Arrangements (Optional):
-
Mocking and Stubbing: In some cases, you might need to mock or stub external dependencies or functions to isolate your unit under test. Vitest offers ways to mock dependencies using functions like
vi.fn()
. These elements come together to form a unit test. You write assertions withinit
blocks to verify the expected behavior of your unit (function, class, module) when the test runner executes the test with specific arrangements (mocking if needed).
-
Mocking and Stubbing: In some cases, you might need to mock or stub external dependencies or functions to isolate your unit under test. Vitest offers ways to mock dependencies using functions like
Let's consider a simple function that calculates the area of a rectangle:
Example 1:- Simple Explanation without mocking
function calculateArea(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Width and height must be positive numbers");
}
return width * height;
}
Here's a unit test for this function using Vitest, highlighting the elements mentioned earlier:
// test file: rectangleArea.test.js
import { test, expect } from 'vitest';
describe('calculateArea function', () => {
// Test case 1: Valid inputs
it('calculates the area correctly for valid dimensions', () => {
const width = 5;
const height = 3;
const expectedArea = 15;
// Test arrangement (no mocking needed here)
const actualArea = calculateArea(width, height);
// Assertions
expect(actualArea).toBe(expectedArea);
});
// Test case 2: Invalid inputs (edge case)
it('throws an error for non-positive width or height', () => {
const invalidWidth = 0;
const validHeight = 3;
// Test arrangement (no mocking needed here)
expect(() => calculateArea(invalidWidth, validHeight)).toThrow();
});
});
Explanation of Elements:
-
Test Runner and Assertions:
- Vitest acts as the test runner, executing the test cases defined within the
describe
andit
blocks. - The
expect
function from Vitest allows us to make assertions about the outcome of the test. Here, we useexpect(actualArea).toBe(expectedArea)
to verify the calculated area matches the expected value.
- Vitest acts as the test runner, executing the test cases defined within the
-
Test Description:
- The
describe
block groups related tests, in this case, all tests for thecalculateArea
function. - Each
it
block defines a specific test case. Here, we have two test cases: one for valid inputs and another for invalid inputs (edge case).
- The
-
Test Arrangements (Optional):
- In this example, we don't need mocking or stubbing as we're directly testing the function with its arguments. However, if the function relied on external dependencies (like file I/O or network calls), we might need to mock them to isolate the unit under test.
Example 2 :- An advance example involving mocking and stubbing
function calculateArea(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Width and height must be positive numbers");
}
return width * height;
}
async function fetchData() {
// Simulate fetching data (could be network call or file read)
return new Promise((resolve) => resolve({ width: 5, height: 3 }));
}
Now, we want to test calculateArea
in isolation without actually making the external call in fetchData
. Here's how mocking comes into play:
// test file: rectangleArea.test.js
import { test, expect, vi } from 'vitest';
describe('calculateArea function', () => {
// Test case 1: Valid dimensions from mocked data
it('calculates the area correctly using mocked dimensions', async () => {
const expectedWidth = 5;
const expectedHeight = 3;
const expectedArea = 15;
// Test arrangement (mocking)
vi.mock('./fetchData', async () => ({ width: expectedWidth, height: expectedHeight }));
// Call the function under test with any values (mocked data will be used)
const actualArea = await calculateArea(1, 1); // Doesn't matter what we pass here
// Assertions
expect(actualArea).toBe(expectedArea);
// Restore mocks (optional, but good practice)
vi.restoreAllMocks();
});
// Other test cases (can remain the same as previous example)
});
Explanation of Mocking:
-
Mocking
fetchData
:- We use
vi.mock
from Vitest to mock thefetchData
function. - Inside the mock implementation (an async function here), we return pre-defined values for
width
andheight
. This way,calculateArea
receives the mocked data instead of making the actual external call.
- We use
-
Test Execution:
- The test case calls
calculateArea
with any values (they won't be used due to mocking). - Since
fetchData
is mocked, the pre-defined dimensions are used for calculation.
- The test case calls
-
Assertions:
- We assert that the
actualArea
matches the expected value based on the mocked data.
- We assert that the
Benefits of Mocking:
- Isolates the unit under test (
calculateArea
) from external dependencies. - Makes tests faster and more reliable (no external calls involved).
- Allows testing specific scenarios with controlled data.
Remember: After each test, it's good practice to restore mocks using vi.restoreAllMocks()
to avoid affecting subsequent tests. This ensures a clean slate for each test case.
Top comments (0)