0. Introduction and Surface level Explanation

sandheep_kumarpatro_1c48

Sandheep Kumar Patro

Posted on June 20, 2024

0. Introduction and Surface level Explanation

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;
Enter fullscreen mode Exit fullscreen mode
// 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']);
  });
});
Enter fullscreen mode Exit fullscreen mode

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 (like data_fetcher.ts) to build the logic in processData. 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:

    1. async function processData(url: string): Promise<string[]>;:
      • This line defines the function named processData. It's async because it might involve waiting for data to be fetched. It takes a url (string) as input and promises to return an array of strings (string[]). This is like laying the foundation for the processing logic.
    2. const data = await fetchData(url);:
      • This line calls the imported fetchData function, passing the provided url. It uses await because fetchData might take time to retrieve data. This is like using a pre-built Lego wall component fetched from another box (the data_fetcher file).
    3. // 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.
    4. 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 uses map 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.
    5. 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.

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:

  1. Mocking the Dependency (Mock Data):
    • In a real scenario, fetchData might fetch data from a server. Here, the test mocks fetchData using vi.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.
  2. Test Execution:
    • The test defines what data to process (url) and asserts (checks) if the returned processedData matches the expected outcome. This is like testing if the built wall section connects properly with the rest of the structure.

Test Case In-depth Explanation:

Imports:

  1. import processData from './data_processor';: This line imports the processData function from the data_processor.ts file. This allows the test to access and test the function.
  2. 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:

  1. vi.mock('./data_fetcher', () =>; ({ fetchData: () =>; mockData }));: This line uses Vitest's vi.mock function to mock the fetchData module from data_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 of fetchData. 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:

  1. describe('processData function', () =>; { ... });: This line defines a test suite using describe 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:

  1. it('should process data from a URL', async () =>; { ... });: This line defines a specific test case using it. The string argument describes the test case ("should process data from a URL"). The async keyword indicates that the test involves asynchronous operations (waiting for the mocked fetchData to return data).

  2. const mockData = 'Line 1\nLine 2\nLine 3';: This line defines a string variable mockData that holds the sample data used in the test. This data will be returned by the mocked fetchData function.

  3. const url = 'https://example.com/data.txt';: This line defines a string variable url that represents the example URL used in the test case. This URL would normally be passed to the real fetchData function, but here it's just a placeholder since we're using mocked data.

  4. const processedData = await processData(url);: This line calls the processData function with the defined url. The await keyword is necessary because processData is asynchronous due to the mocked fetchData. This line essentially simulates calling processData with a URL and waits for the processed data to be returned.

  5. expect(processedData).toEqual(['Line 1', 'Line 2', 'Line 3']);: This line is the assertion using Vitest's expect. It checks if the processedData returned by processData is equal to the expected array containing the processed lines ("Line 1", "Line 2", "Line 3"). This verifies if processData 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).

💖 💪 🙅 🚩
sandheep_kumarpatro_1c48
Sandheep Kumar Patro

Posted on June 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related