1. Understanding unit test basics
Tags: react, vitest, unittest, javascript
Published: 2024-06-20 18:49:33

### **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:

```javascript
// area.ts
export function calculateArea(width: number, height: number): number {
  return width * height;
}
```

```javascript
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 for `calculateArea`.

- Within the `describe` block, we define two test cases using `it`:

    - The first test verifies if the function calculates the area correctly for non-zero dimensions.

        - We define `width`, `height`, and `expectedArea`.

        - We call `calculateArea` with the defined values.

        - We use `expect` to assert that the actual area (`actualArea`) matches the expected area.

    - 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`, and `expectedArea`.

        - We call `calculateArea` with these values and assert the result using `expect`.



### **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:
1. **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.
2. **Test Description:**
    - **`describe` and `it` 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 using `it` blocks. Each `it` block describes a specific scenario you want to test.
3. **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 within `it` 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).



Let's consider a simple function that calculates the area of a rectangle:

**Example 1:- Simple Explanation without mocking**

```javascript
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:

```javascript
// 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:**
1. **Test Runner and Assertions:**
    - Vitest acts as the test runner, executing the test cases defined within the `describe` and `it` blocks.
    - The `expect` function from Vitest allows us to make assertions about the outcome of the test. Here, we use `expect(actualArea).toBe(expectedArea)` to verify the calculated area matches the expected value.
2. **Test Description:**
    - The `describe` block groups related tests, in this case, all tests for the `calculateArea` 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).
3. **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**

```javascript
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:

```javascript
// 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:**
1. **Mocking `fetchData`:**
    - We use `vi.mock` from Vitest to mock the `fetchData` function.
    - Inside the mock implementation (an async function here), we return pre-defined values for `width` and `height`. This way, `calculateArea` receives the mocked data instead of making the actual external call.
2. **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.
3. **Assertions:**
    - We assert that the `actualArea` matches the expected value based on the mocked data.

**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.