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
expectfrom Vitest for assertions.We import
calculateAreafrom our area.ts file.We use
describeto create a test suite forcalculateArea.-
Within the
describeblock, 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
calculateAreawith the defined values. - We use
expectto 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
forloop. - For each case, we extract
width,height, andexpectedArea. - We call
calculateAreawith 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:
-
describeanditblocks: These functions from Vitest (similar to other frameworks) structure your tests.-
describedefines a test suite that groups related tests for a specific functionality. - Within
describe, individual test cases are defined usingitblocks. Eachitblock 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 withinitblocks 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
describeanditblocks. - The
expectfunction 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
describeblock groups related tests, in this case, all tests for thecalculateAreafunction. - Each
itblock 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.mockfrom Vitest to mock thefetchDatafunction. - Inside the mock implementation (an async function here), we return pre-defined values for
widthandheight. This way,calculateAreareceives the mocked data instead of making the actual external call.
- We use
-
Test Execution:
- The test case calls
calculateAreawith any values (they won't be used due to mocking). - Since
fetchDatais mocked, the pre-defined dimensions are used for calculation.
- The test case calls
-
Assertions:
- We assert that the
actualAreamatches 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)