0. Introduction and Surface level Explanation
Sandheep Kumar Patro
Posted on June 20, 2024
Let me start with a story
Imagine you're building a big Lego castle. Unit testing is like checking each individual Lego brick before you snap them together.
Small pieces: Instead of testing the entire castle at once, you test each brick (the tiny building block). In software, these bricks are functions, small pieces of code that do one specific thing.
Working alone: You check if each brick can connect properly on its own, without needing the whole castle built yet. In code, this means testing the function with different inputs (like numbers) to see if it gives the expected output (like the answer).
Catching problems early: If a brick is broken or bent, you find it before wasting time building with it. In code, this helps catch errors early on, before they cause bigger problems later in the whole program.
So, unit testing is basically making sure the tiny building blocks of software work correctly before putting everything together. This helps catch mistakes early and make the final program run smoothly!
Small taste of code now,
// Source code (to be unit tested)
import { fetchData } from './data_fetcher'; // Import the data fetching function
async function processData(url: string): Promise<string[]> {
"""
Fetches data from a URL, parses it, and returns an array of strings.
This function depends on the `fetchData` function to retrieve data.
"""
const data = await fetchData(url);
// Simulate parsing data (replace with your actual parsing logic)
const processedData = data.split('\n').map(line => line.trim());
return processedData;
}
export default processData;
// Code for unit testing
import processData from './data_processor';
import { expect, vi } from 'vitest'; // Use Vitest's built-in expect and vi for mocking
describe('processData function', () => {
it('should process data from a URL', async () => {
const mockData = 'Line 1\nLine 2\nLine 3';
vi.mock('./data_fetcher', () => ({ fetchData: () => mockData })); // Mock using vi.mock
const url = 'https://example.com/data.txt';
const processedData = await processData(url);
expect(processedData).toEqual(['Line 1', 'Line 2', 'Line 3']);
});
});
Explanation by comparing
The provided code for processData
and its test case can be compared to building a big Lego castle in the following ways:
Castle Foundation (Imports):
Castle: Before building, you gather all the necessary bricks (Lego pieces) you'll need.
Code: Similar to gathering bricks, the
import
statements (e.g.,import { fetchData } from './data_fetcher'
) bring in the required functionality from other files (likedata_fetcher.ts
) to build the logic inprocessData
. These imported functions act as pre-built Lego components.
Castle Walls (Function Definition):
Castle: The castle walls are the main structure, built with various Lego bricks.
Code: The
processData
function is like the main structure of the code. It defines the steps to process data, similar to how instructions guide the castle assembly.
Castle Details (Function Lines):
Castle: Each line of the building instructions specifies how to place individual bricks.
-
Code: Each line of code within
processData
represents an action or step. Let's break them down:-
async function processData(url: string): Promise<string[]>;
:- This line defines the function named
processData
. It'sasync
because it might involve waiting for data to be fetched. It takes aurl
(string) as input and promises to return an array of strings (string[]
). This is like laying the foundation for the processing logic.
- This line defines the function named
-
const data = await fetchData(url);
:- This line calls the imported
fetchData
function, passing the providedurl
. It usesawait
becausefetchData
might take time to retrieve data. This is like using a pre-built Lego wall component fetched from another box (thedata_fetcher
file).
- This line calls the imported
-
// Simulate parsing data (replace with your actual parsing logic)
:- This line is a comment explaining that the following code simulates parsing data (splitting and trimming lines). You'd replace this with your actual logic for processing the fetched data. This is like the specific steps for building a unique part of the castle wall with different colored or shaped bricks.
-
const processedData = data.split('\n').map(line =>; line.trim());
:- This line performs the actual data processing. It splits the fetched
data
by newline characters (\n
) and then usesmap
to iterate over each line, trimming any whitespace (trim
). This is like assembling the fetched data wall component by splitting and connecting individual Lego pieces.
- This line performs the actual data processing. It splits the fetched
-
return processedData;
:- This line returns the final processed data (
processedData
) as an array of strings. This is like presenting the completed wall section that you built.
- This line returns the final processed data (
-
Testing the Castle (Test Case):
Castle: After building, you might check the stability and functionality of different parts.
Code: The test case (in a separate file) simulates checking the functionality of
processData
.
Test Case Breakdown:
-
Mocking the Dependency (Mock Data):
- In a real scenario,
fetchData
might fetch data from a server. Here, the test mocksfetchData
usingvi.mock
(Vitest) to control the returned data (e.g.,mockData
). This is like creating a mock wall section without fetching real bricks, just to test how the main structure connects.
- In a real scenario,
-
Test Execution:
- The test defines what data to process (
url
) and asserts (checks) if the returnedprocessedData
matches the expected outcome. This is like testing if the built wall section connects properly with the rest of the structure.
- The test defines what data to process (
Test Case In-depth Explanation:
Imports:
-
import processData from './data_processor';
: This line imports theprocessData
function from thedata_processor.ts
file. This allows the test to access and test the function. -
import { expect, vi } from 'vitest';
: Here, we import two functionalities from Vitest:-
expect
: This is used for making assertions about the test results. -
vi
: This provides utilities for mocking in Vitest.
-
Mocking the Dependency:
-
vi.mock('./data_fetcher', () =>; ({ fetchData: () =>; mockData }));
: This line uses Vitest'svi.mock
function to mock thefetchData
module fromdata_fetcher.ts
. Mocking essentially creates a fake version of the module for testing purposes.-
./data_fetcher
: This specifies the path to the module being mocked. -
() =>; ({ fetchData: () =>; mockData })
: This is an anonymous function that defines the mocked behavior offetchData
. Here, it simply returns a predefined string (mockData
) instead of actually fetching data.
-
Why Mocking?
Mocking is important in this scenario because the real fetchData
function might involve network calls or interact with external systems. These can introduce external factors that could make the test unreliable or slow down execution. By mocking, we control the data returned by fetchData
and isolate the test to focus solely on how processData
handles the provided data.
Test Description:
-
describe('processData function', () =>; { ... });
: This line defines a test suite usingdescribe
from Vitest. It groups related tests under the descriptive name "processData function". The code within the curly braces ({...}
) will contain individual test cases.
Individual Test Case:
it('should process data from a URL', async () =>; { ... });
: This line defines a specific test case usingit
. The string argument describes the test case ("should process data from a URL"). Theasync
keyword indicates that the test involves asynchronous operations (waiting for the mockedfetchData
to return data).const mockData = 'Line 1\nLine 2\nLine 3';
: This line defines a string variablemockData
that holds the sample data used in the test. This data will be returned by the mockedfetchData
function.const url = 'https://example.com/data.txt';
: This line defines a string variableurl
that represents the example URL used in the test case. This URL would normally be passed to the realfetchData
function, but here it's just a placeholder since we're using mocked data.const processedData = await processData(url);
: This line calls theprocessData
function with the definedurl
. Theawait
keyword is necessary becauseprocessData
is asynchronous due to the mockedfetchData
. This line essentially simulates callingprocessData
with a URL and waits for the processed data to be returned.expect(processedData).toEqual(['Line 1', 'Line 2', 'Line 3']);
: This line is the assertion using Vitest'sexpect
. It checks if theprocessedData
returned byprocessData
is equal to the expected array containing the processed lines ("Line 1", "Line 2", "Line 3"). This verifies ifprocessData
correctly parses the mocked data.
Running the Test:
In your terminal, you can run the tests using npm vitest
. Vitest will execute the test suite and report if all assertions pass (meaning the code works as expected) or fail (meaning there's an error in the processData
function).
Posted on June 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.