Mocking the composable

In this fourth episode, we will mock the composable. This will help us isolate the component test without relying on Nuxt's real interactions or localStorage behavior.

8 min read
Share
Mocking the composable

Component Reminder: VersionBanner02.vue

👉 Code: VersionBanner02.vue

<script lang="ts" setup>
// File: layers/version-02/components/VersionBanner02.vue
import { useVersion } from "../composables/useVersion";
const { isVisible, version, close } = useVersion();
</script>

<template>
  <div v-if="isVisible">
    (02) New Version {{ version }}
    <button @click="close">Close</button>
  </div>
</template>

First Mock (v2-2)

👉 Code: version-banner-2-2.unit.spec.ts

// File: layers/version-02/__tests__/version-banner.spec.ts
// @vitest-environment happy-dom
import { describe, it, expect, vi } from "vitest";
import { shallowMount } from "@vue/test-utils";
import VersionBanner02 from "../components/VersionBanner02.vue";

// Mock the useVersion module.
vi.mock("../composables/useVersion", () => ({
  useVersion: () => ({
    isVisible: true, // Forces the banner to display and pass the test.
    version: "0.0.1",
    close: () => {},
  }),
}));

describe("VersionBanner", () => {
  it("should display the version", () => {
    const wrapper = shallowMount(VersionBanner02);
    expect(wrapper.text()).toContain("0.0.1");
  });
});

Note: The vi.mock function instructs Vitest to mock the ../composables/useVersion module. Vitest intercepts the import and replaces the real module with our mock version.

Benefits of This Approach

We have successfully isolated the component from the composable’s interactions with runtimeConfig and localStorage.

Complete Component Test With Composable Mocking (v2-3)

👉 Code: VersionBanner02.vue

First, let’s add data-testid attributes to the component. This common technique lets us select elements without being affected by changes in the internal HTML layout.

<template>
  <div v-if="isVisible" data-testid="version-banner">
    New Version {{ version }}
    <button @click="close" data-testid="version-close">Close</button>
  </div>
</template>

Now, we refactor the test to eliminate redundancy and add mocks for the functions we want to track (like close()).

Note: The expectations have changed. We are not testing the logic that compares localStorage with package.json—this logic should be covered in tests dedicated to the composable. Here, we focus on verifying that the component correctly connects to the composable, handles UI conditional rendering, and displays the proper HTML.

Code: version-banner-2-3.unit.spec.ts

// @vitest-environment happy-dom
import { describe, it, expect, vi, beforeEach } from "vitest";
import { shallowMount, VueWrapper } from "@vue/test-utils";
import VersionBanner02 from "../components/VersionBanner02.vue";
import { useVersion } from "../composables/useVersion";

vi.mock("../composables/useVersion", () => ({
  useVersion: vi.fn(),
}));

describe("VersionBanner", () => {
  let wrapper: VueWrapper;

  // Default mock return value
  const mockVersionData: ReturnType<typeof useVersion> = {
    isVisible: ref(true),
    version: "0.0.1",
    close: vi.fn(), // Tracking the close function
  };

  beforeEach(() => {
    vi.mocked(useVersion).mockReturnValue(mockVersionData);
    wrapper = shallowMount(VersionBanner02);
  });

  // Helper functions to access elements in the wrapper.
  const getBanner = () => wrapper.find('[data-testid="version-banner"]');
  const getCloseButton = () => wrapper.find('[data-testid="version-close"]');

  it("should display the version", () => {
    expect(getBanner().text()).toContain(mockVersionData.version);
  });

  it("should display the version banner when isVisible is true", () => {
    expect(getBanner().exists()).toBe(true);
  });

  it("should have a close button", () => {
    expect(getCloseButton().exists()).toBe(true);
  });

  it("should not display the version banner when isVisible is false", () => {
    // Adjusting the mock to set isVisible to false.
    vi.mocked(useVersion).mockReturnValue({
      ...mockVersionData,
      isVisible: ref(false),
    });

    wrapper = shallowMount(VersionBanner02);
    expect(getBanner().exists()).toBe(false);
  });

  // We don't test the hiding logic here—this behavior is defined in the composable.
  it("should call close() when the close button is clicked", async () => {
    await getCloseButton().trigger("click");
    expect(mockVersionData.close).toHaveBeenCalledOnce();
  });
});

In these tests, we’re verifying that:

Benefits

By refactoring, we have improved the test structure in several ways:

The Problem With Mocking Composables

There is a downside to this approach:

Should We Avoid Mocks?

There’s an ongoing debate in testing:

For example, checking that mockVersionData.close has been called is a white-box approach—do we really want the component to care about the composable’s implementation? It might be clearer to test that the button emits a certain event.

Next Step

To simplify the tests further, we might consider completely isolating the component from the composable by wrapping it in a parent component. This change would help focus tests solely on the component’s behavior rather than the inner workings of its dependencies.

What do you think? Should we continue refining our testing strategy further?

Specifications v3

Decision Map

Let’s take a look at the current map (Open 🔎):

Decision Map Graph

Previous

Next