An advanced guide to Vitest testing and mocking

leemeganj

Megan Lee

Posted on June 11, 2024

An advanced guide to Vitest testing and mocking

Written by Sebastian Weber
✏️

Testing is crucial in modern software development for ensuring code quality, reliability, and maintainability. However, the complexity of testing can often feel overwhelming.

In this article, we will first delve into real-world use cases to demonstrate testing strategies using Vitest and its various APIs. We will also share a cheat sheet with condensed Vitest examples that can serve as a useful resource for both aspiring testers and experienced developers.

Let’s get started!

An introduction to testing

If you are new to testing, it is worth familiarizing yourself with some of its important terms and concepts:

  • Test doubles: An umbrella term for the following concepts
  • Dummies: Objects or primitive values that are passed around and sometimes not even used. They are typically used as function arguments (e.g., a hard-coded customer object)
  • Fakes: Functions that have working implementations but take shortcuts that make them unsuitable for production, such as an in-memory database
  • Stubs: Functions that provide ready-made answers to the calls made during the test and generally do not respond to anything that has not been programmed for the test
  • Spies: Stubs that also record some information about how they are used, e.g., with which arguments a spied function is called
  • Mocks: Pre-set functions designed to anticipate and specify the calls they should receive, helping in the verification of expected behavior

Because there are no official definitions, there are differing opinions on the differences between spies and mocks. This article defines spies as tools that do not change the original implementation of a module, using real actors to verify expected interactions. In contrast, mocks are fully or partly replaced modules with simplified functions to control tests. In your tests, you don't want to make actual network calls, so you have to replace the module responsible for it with a mock.

For a deeper dive into testing, check out this guide to unit testing, a guide to unit and integration testing for Node.js apps, and a Vitest tutorial for automated testing using Vue components.

Now, let’s explore in detail how to write tests with Vitest.

A deep dive into testing with Vitest

The goal of this article is to teach you how to use Vitest's API to write robust, readable, and maintainable tests. If you want to learn more about Vitest architecture, I recommend Ben Holmes's video, which shows that Vitest features built-in modern design decisions (e.g., supporting ESM modules).

Before we delve into the code examples, let’s first set up a good test workflow.

Setting up a testing workflow

Establishing a good testing workflow can greatly enhance productivity. By installing Vitest’s official extension for VS Code, you can streamline your testing process. It’s a good addition to Vitest's CLI by providing a graphical interface for debugging tests as well as running and visualizing code coverage.

You can start debugging multiple test files or a single test by clicking on the play button with the bug icon: Debugging Tests Using The Vitest.Explorer Extension You can further enhance your workflow by incorporating code coverage analysis. This provides insight into the effectiveness of your tests by indicating which parts of your codebase are covered by tests and which are not.

Before you start, you need to install a coverage provider. Depending on your preference, you can choose between the v8 or istanbul packages:

$ npm i -D @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode

Then, you have to configure the coverage provider in vitest.config.ts:

// ...
export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
        // ...
        root: fileURLToPath(new URL("./", import.meta.url)),
        coverage: {
            provider: "v8",
        },
    },
  }),
);
Enter fullscreen mode Exit fullscreen mode

With your provider in place, you can run a test by clicking the run test with coverage button. The Test Explorer view provides a visual demonstration of this: Running Tests With Coverage From VS Code Alternatively, you can run tests with coverage from the terminal ($ vitest run --coverage): Running Tests With Coverage From CLI Another good way to work on your tests is to run the Vitest CLI in watch mode:

$ vitest # watch mode is the default
Enter fullscreen mode Exit fullscreen mode

By pressing the H key, you can open the menu to run failed tests or all tests. Whenever you save a file, the containing tests are rerun: Vitest CLI In Watch Mode

Testing a service function performing a network call

Now, let's look at our first test. All the examples in this article are taken from the companion GitHub project. Consider the following service function, fetchQuote:

// quote.service.ts
import type { Quote } from "./types/quote";
export async function fetchQuote() {
  const response = await fetch("https://dummyjson.com/quotes/random");
  const data: Quote = await response.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

fetchQuote performs a network call by using the global fetch method. The returned fetch response is of type Quote.

One approach is to create a spy to perform behavior verification after the test candidate, the fetchQuote function, is invoked. The spy tests whether the global fetch call has been called with the correct parameters. In addition, we can perform a state verification of the response:

// quote.service.spy.test.ts
import { describe, it, expect, vi } from "vitest";
import { fetchQuote } from "./quote.service";
describe("fetchQuote", () => {
  it("should return a valid quote", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch");
    // invoke the test candidate
    const response = await fetchQuote();
    // behavior verification
    expect(fetchSpy).toHaveBeenCalledWith(
      "https://dummyjson.com/quotes/random",
    );
    // state verification
    expect(response.quote).toBeDefined();
  });
});
Enter fullscreen mode Exit fullscreen mode

As we've learned, a spy usually doesn’t alter the implementation, so a real network call is invoked. This isn’t ideal because it means that test runs are not deterministic. We can demonstrate this with a snapshot test by adding the following line:

// ...
const response = await fetchQuote();
// ...
expect(response).toMatchSnapshot();
Enter fullscreen mode Exit fullscreen mode

If you run the test twice, you’ll get a snapshot mismatch because the response object has different contents: Snapshot Testing Reveals That This Is Not A Deterministic Test In addition, we can use a rudimentary state verification because we can only test for the existence of the quote object:

expect(response.quote).toBeDefined();
Enter fullscreen mode Exit fullscreen mode

Let's look at a better approach for this testing scenario by mocking the service:

// quote.service.mock.test.ts
import { describe, it, expect, vi } from "vitest";
import { fetchQuote } from "./quote.service";
import type { Quote } from "./types/quote";
vi.mock("./quote.service");
describe("fetchQuote", () => {
  it("should return a valid quote", async () => {
    const dummyQuote: Quote = {
      id: 1,
      quote: "This is a dummy quote",
      author: "Anonymous",
    };
    vi.mocked(fetchQuote).mockResolvedValue(dummyQuote);
    expect(await fetchQuote()).toEqual(dummyQuote);
  });
});
Enter fullscreen mode Exit fullscreen mode

But wait — this isn’t a good test at all! It tests nothing because we replaced our test candidate with a mock. The test candidate has to be the unaltered original because we want to test its correct functionality. Let's mock the network call and define what should be returned by global fetch:

// quote.service.mock.test.ts
import { describe, it, expect, vi } from "vitest";
import { fetchQuote } from "./quote.service";
import type { Quote } from "./types/quote";
describe("fetchQuote", () => {
  it("should return a valid quote", async () => {
    // just a dummy quote
    const dummyQuote: Quote = {
      id: 1,
      quote: "This is a dummy quote",
      author: "Anonymous",
    };
    // needs to contain the required properties 
    // https://developer.mozilla.org/en-US/docs/Web/API/Response 
    const mockResponse = {
      ok: true,
      statusText: "OK",
      json: async () => dummyQuote,
    } as Response;
    // this time mock global fetch instead of spying on it
    globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
    // state verification
    expect(await fetchQuote()).toEqual(dummyQuote);
  });
});
Enter fullscreen mode Exit fullscreen mode

This is an example of a state verification by checking whether fetchQuote's return value matches the response of the network call initiated by fetch.

Don't worry — this test looks much more complicated than it is because we have to understand the signature of fetch. As you can see in the documentation, it returns a promise that resolves to the Response object representing the response to your request. This is the fetch interface:

  // node_modules/@types/node/globals.d.ts
  // ...
  function fetch(
    input: string | URL | globalThis.Request,
      init?: RequestInit,
  ): Promise<Response>;
  // ...
Enter fullscreen mode Exit fullscreen mode

In our case, we defined a dummy object representing an object of type Quote. We return this dummy in the JSON response when the promise gets resolved. Therefore, we need to use Vitest's mockResolvedValue, which is available for test doubles such as mocks and spies:

  // ...
  const mockResponse = {
    // ...
    json: async () => dummyQuote,
  } as Response;
  globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
  // ...
Enter fullscreen mode Exit fullscreen mode

We can also refactor our previous test (quote.service.spy.test.ts) in a way that the spy returns our fake response:

  it("should return a valid quote by returning a fake response", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
      ok: true,
      statusText: "OK",
      json: async () => ({ quote: "Hello, World!" }),
    } as Response);
    const response = await fetchQuote();
    expect(fetchSpy).toHaveBeenCalledWith(
      "https://dummyjson.com/quotes/random",
    );
    expect(response.quote).toBe("Hello, World!");
  });
Enter fullscreen mode Exit fullscreen mode

Besides looking into the documentation or the globals' types, we can also make use of debugging to find out the internals of external modules.

Testing the correct rendering of a Vue component

Testing the correct rendering of the App.vue component is an example of white box testing:

<template>
  <h1>My awesome dashboard</h1>
  <img :src="imageUrl" :alt="imgAlt" />
  <Counter />
  <TodoFromStore />
  <TodoFromComposable />
</template>
<script setup lang="ts">
// hide imports
const dashboardStore = useDashboardStore();
const imageUrl = ref("");
const imgAlt = ref("");
onMounted(async () => {
  const blob = await dashboardStore.createQuoteImageWithComposable();
  if (blob) {
    imageUrl.value = URL.createObjectURL(blob);
    imgAlt.value = dashboardStore.shortenedQuote;
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

You will shortly find out that this component does not have a good design in terms of testability. This is how to test it:

// App.test.ts
import { flushPromises, shallowMount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import App from "./App.vue";
import { createPinia, defineStore } from "pinia";
describe("App", () => {
  it("renders the image correctly", async () => {
    // mock URL.createObjectURL since it is an internal of the onMounted hook
    vi.stubGlobal("URL", { createObjectURL: () => "a nice URL" });
    // Create a mock store
    const useMockDashboardStore = defineStore("dashboard", () => ({
      createQuoteImageWithComposable: async () => {
        // Create a dummy blob
        const dummyBlob = new Blob();
        return Promise.resolve(dummyBlob);
      },
      shortenedQuote: "Dummy shortened quote",
    }));
    // init pinia and use the mock store
    const pinia = createPinia();
    useMockDashboardStore(pinia);
    // shallow mount the App component
    // only the first (component tree) level of Vue components are rendered
    const wrapper = shallowMount(App);
    // make sure to invoke onMounted lifecycle hook and resolve the promise
    await flushPromises();
    const imgEl = wrapper.find("img");
    expect(imgEl.attributes().alt).toBe("Dummy shortened quote");
    expect(imgEl.attributes().src).toBe("a nice URL");
    // renders only first child level of App component: h1, img, tags of included Vue components
    expect(wrapper.html()).toMatchSnapshot();
  });
});
Enter fullscreen mode Exit fullscreen mode

The above code is complex, so let's break it down. The first question is how to deal with Vue components. We will combine Vitest with Vue Test Utils, which is a library of helper functions to help users test their Vue components.

In this case, we can use either the mount or shallowMount functions to mount the Vue component. We’ll opt for the latter because it stubs out all children of the App component.

Testing the App component in isolation shouldn’t include implementation details of child components like Counter. Without shallowMount, we open up another can of worms because we also have to mock the dependencies of the child components.

Switching from shallowMount(App) to mount(App) breaks our test because of the child component internals: Switching From Shallow Mount To Mount Breaks The Test We also need to set up a mock Pinia store (dashboard) because the App component accesses an action (createQuoteImageWithComposable) and a getter (shortenedQuote) in the onMounted lifecycle Hook:

// Create a mock store
  const useMockDashboardStore = defineStore("dashboard", () => ({
    createQuoteImageWithComposable: async () => {
      const dummyBlob = new Blob();
      return Promise.resolve(dummyBlob);
    },
    shortenedQuote: "Dummy shortened quote",
  }));
  const pinia = createPinia();
  useMockDashboardStore(pinia);
  const wrapper = shallowMount(App);
Enter fullscreen mode Exit fullscreen mode

Providing a mock store for testing doesn’t require using Vitest's API functions — we can just create a store with defineStore that exposes the API that App's component uses.

Inside the onMounted Hook, store.createQuoteImageWithComposable() needs to return a promise that resolves to a blob response, while store.shortenedQuote is just a string.

Due to an unfavorable implementation, we need to stub URL.createObjectURL as it generates a URL by the returned blob:

vi.stubGlobal("URL", { createObjectURL: () => "a nice URL" })
Enter fullscreen mode Exit fullscreen mode

In a minute, I’ll suggest how to improve the App component to simplify this test.

Because this component relies on a lifecycle hook, we need to utilize another API function of Vue Test Utils (flushPromises). It is important to wait with asserting values until all DOM updates are done and all pending promises are resolved:

await flushPromises();
Enter fullscreen mode Exit fullscreen mode

This is why our test needs to be async:

it("renders the image correctly", async () => {
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Now we can make our assertions:

const imgEl = wrapper.find("img");
expect(imgEl.attributes().alt).toBe("Dummy shortened quote");
expect(imgEl.attributes().src).toBe("a nice URL");
// renders only first child level of App component: h1, img, tags of included Vue components
expect(wrapper.html()).toMatchSnapshot();
Enter fullscreen mode Exit fullscreen mode

Because we used shallowMount, the snapshot looks like this:

// __snapshots__/App.test.ts.snap
exports[`App > renders the image correctly 1`] = `
"<h1>My awesome dashboard</h1>
<img src="a nice URL" alt="Dummy shortened quote">
<counter-stub></counter-stub>
<todo-from-store-stub></todo-from-store-stub>
<todo-from-composable-stub></todo-from-composable-stub>"
`;
Enter fullscreen mode Exit fullscreen mode

Improving testability

Let’s make our lives easier by refactoring the onMounted Hook:

// see AppRefactored.vue
onMounted(async () => {
  const image = await dashboardStore.createQuoteImageWithComposableRefactored();
  if (image) {
    imageUrl.value = image.url;
    imgAlt.value = image.altText;
  }
});
Enter fullscreen mode Exit fullscreen mode

The store function is also refactored to provide an image URL and alt text right away:

// return type of createQuoteImageWithComposableRefactored
 type Image = { url: string; altText: string };
Enter fullscreen mode Exit fullscreen mode

The improved signature of the store function makes the App component easier to test because of better separation of concerns. The store method is completely in charge of providing the data to render. This means that we can get rid of stubbing out of URL.createObjectURL because it was extracted into the store method.

Testing store actions in isolation

If you place complex logic into store actions and invoke them from Vue components, you can test this logic in isolation. Let's look at the example action used in the App component:

// store.ts
const createQuoteImageWithComposable = async () => {
  const blob: Ref<Blob | null> = ref(null);
  const jsonState = await useFetch<Quote>(
    "https://dummyjson.com/quotes/random",
  );
  if (!jsonState.hasError) {
    currentQuote.value = jsonState.data;
    const blobState = await useFetch<Blob>(
      `https://dummyjson.com/image/768x80/008080/ffffff?text=${jsonState.data?.quote}`,
      { responseType: "blob" },
    );
    if (!blobState.hasError) {
      blob.value = blobState.data;
    }
  }
  return toValue(blob);
};
Enter fullscreen mode Exit fullscreen mode

This action makes two network calls with an imported module (useFetch) to get two different responses. The challenge is to mock this imported useFetch composable in a way that the first call returns JSON and the second one a blob response.

First, in order to mock the network calls in the arranging phase, we need to know the signature of the generic useFetch composable. We will look at the test of useFetch in a minute, but all we need to know right now is the signature:

interface State<T> {
  isLoading: boolean;
  hasError: boolean;
  error: Error | null;
  data: T | null;
}
// signature of useFetch
function useFetch<T>(url: string, options?: {
    responseType?: "json" | "blob";
}): Promise<State<T>>
Enter fullscreen mode Exit fullscreen mode

Our mock implementation of useFetch can ignore the arguments but we need to make sure to receive the correct fetch state objects:

// store.test.ts
import { useFetch } from "./composables/useFetch";
// ...
vi.mock("./composables/useFetch");
// ...
it("createQuoteImageWithComposable should create a quote image by calling useFetch twice", async () => {
    // arrange
    const dummyJsonState = {
      data: { id: 1, quote: "Hello, World!", author: "Anonymous" },
      hasError: false,
    };
    const dummyBlob = new Blob();
    const mockedUseFetch = vi.mocked(useFetch) as Mock;
    mockedUseFetch
      .mockResolvedValueOnce(dummyJsonState)
      .mockResolvedValueOnce({ data: dummyBlob, hasError: false });
    // act / call the test candidate
    const blob = await store.createQuoteImageWithComposable();
    // state assertion
    expect(blob).toBe(dummyBlob);
    // behaviour assertions
    // check the arguments of the first useFetch call
    expect(mockedUseFetch.mock.calls\[0\][0]).toBe(
      "https://dummyjson.com/quotes/random",
    );
    // check the arguments of the second useFetch call
    expect(mockedUseFetch.mock.calls\[1\][0]).toBe(
      `https://dummyjson.com/image/768x80/008080/ffffff?text=${dummyJsonState.data.quote}`,
    );
  });
Enter fullscreen mode Exit fullscreen mode

The comments inside of the previous snippet emphasize the different phases of a test: arrange (initialization of test doubles), act (invoking the test candidate), and assert (verification of correct behavior or result of test candidate).

The approach used in the following test is to mock the whole module:

vi.mock("./composables/useFetch")
Enter fullscreen mode Exit fullscreen mode

vi.mock gets hoisted to the top of the file, so the position in the test file does not matter. In addition, we need to provide the actual import:

import { useFetch } from "./composables/useFetch"`)
Enter fullscreen mode Exit fullscreen mode

With that in place, we can create dummy return values that our mocked fetch calls should return:

// arrange the test
const dummyJsonState = {
  data: { id: 1, quote: "Hello, World!", author: "Anonymous" },
  hasError: false,
};
const dummyBlob = new Blob();
const mockedUseFetch = vi.mocked(useFetch) as Mock;
mockedUseFetch
  .mockResolvedValueOnce(dummyJsonState)
  .mockResolvedValueOnce({ data: dummyBlob, hasError: false });
Enter fullscreen mode Exit fullscreen mode

With vi.mocked, we get access to the mocked function and assign it to mockedUseFetch. With the help of mockResolvedValueOnce, we can resolve the first promise with a JSON state and the second promise with a blob state.

Finally, we can make our assertions:

// state assertion
expect(blob).toBe(dummyBlob);
// ...
// check the arguments of the second useFetch call
expect(mockedUseFetch.mock.calls\[1\][0]).toBe(
`https://dummyjson.com/image/768x80/008080/ffffff?text=${dummyJsonState.data.quote}`,
);
Enter fullscreen mode Exit fullscreen mode

Testing composables in isolation

In the previous test, we mocked useFetch because it constitutes an external dependency of the test candidate (store action createQuoteImageWithComposable). Next, we'll test useFetch in isolation:

// useFetch.ts
export async function useFetch<T>(
  url: string,
  options: { responseType?: "json" | "blob" } = { responseType: "json" },
) {
  const fetchState = reactive<State<T>>({
    isLoading: false,
    hasError: false,
    error: null,
    data: null,
  });
  try {
    fetchState.isLoading = true;
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    fetchState.data =
      options.responseType === "json"
        ? await response.json()
        : await response.blob();
  } catch (err: unknown) {
    fetchState.hasError = true;
    fetchState.error = err as Error;
    // throw new Error(fetchState.error.message);
  } finally {
    fetchState.isLoading = false;
  }
  return fetchState;
}
Enter fullscreen mode Exit fullscreen mode

The challenge is to deal with the global fetch function that makes actual network calls. For test stability and efficiency reasons, we do not want to perform actual fetch calls so, we’ll substitute it with a mock.

The corresponding test shows one approach to mock global fetch, but there are multiple ways to do this as you will see in the cheat sheet section of this article. Let's look at the happy path first, where useFetch returns a JSON valid response, i.e., the network call is successful:

// useFetch.test.ts
import { useFetch } from "./useFetch";
globalThis.fetch = vi.fn();
describe("useFetch", () => {
  it("should fetch data successfully and return the response", async () => {
    // arrange
    const dummyData = { message: "Hello, World!" };
    const mockResponse = {
      ok: true,
      statusText: "OK",
      json: async () => dummyData,
    } as Response;
    vi.mocked(fetch).mockResolvedValue(mockResponse);
    // act
    const response = await useFetch("https://api.example.com/data");
    // state assertions
    expect(response.isLoading).toBe(false);
    expect(response.hasError).toBe(false);
    expect(response.error).toBe(null);
    expect(response.data).toEqual(dummyData);
    // behavior assertions
    expect(vi.mocked(fetch)).toHaveBeenCalledTimes(1);
    expect(vi.mocked(fetch)).toHaveBeenCalledWith(
      "https://api.example.com/data",
    );
  });

  // ...
});
Enter fullscreen mode Exit fullscreen mode

We set the properties of the response mock object to represent a successful network call. Because we want to receive JSON data, we add a json property to the mockResponse object. We create a dummy data object that gets returned when the promise is resolved (dummyData). Finally, we order Vitest to return our mockResponse when a fetch call is made:

vi.mocked(fetch).mockResolvedValue(mockResponse);
Enter fullscreen mode Exit fullscreen mode

In the acting phase, we will call the test candidate and store the mocked response in a variable:

const response = await useFetch("https://api.example.com/data");
Enter fullscreen mode Exit fullscreen mode

Finally, we make state and behavior assertions and test whether the fetch state looks as expected.

Next is an example of testing a failing network call with the help of the mockRejectedValue API function:

// useFetch.test.ts
import { useFetch } from "./useFetch";
globalThis.fetch = vi.fn();
// ...
it("should set the error state correctly when fetch request gets rejected", async () => {
  const errorMessage = "Network error";
  vi.mocked(fetch).mockRejectedValue(new Error(errorMessage));
  const response = await useFetch("https://api.example.com/data");
  expect(response.isLoading).toBe(false);
  expect(response.hasError).toBe(true);
  expect(response.error!.message).toEqual(errorMessage);
  expect(response.data).toBe(null);
});
Enter fullscreen mode Exit fullscreen mode

Testing timers

As the last example in this chapter, we’ll examine a rather complex composable, useFetchTodoWithPolling. The goal is to re-fetch to-dos at intervals after a defined number of milliseconds (parameter pollingInterval):

// useFetchTodoWithPolling.ts
import { ref, type Ref } from "vue";
import { useFetch } from "./useFetch";
interface Todo {
  id: number;
  todo: string;
  completed: boolean;
  userId: number;
}
export const useFetchTodoWithPolling = (pollingInterval: number) => {
  const todo: Ref<Todo | null> = ref(null);
  const doPoll = ref(true);
  const poll = async () => {
    try {
      if (doPoll.value) {
        const fetchState = await useFetch<Todo>(
          "https://dummyjson.com/todos/random",
        );
        todo.value = fetchState.data;
        setTimeout(poll, pollingInterval);
      }
    } catch (err: unknown) {
      console.error(err);
    }
  };
  poll();
  const togglePolling = () => {
    doPoll.value = !doPoll.value;
    if (doPoll.value) {
      poll();
    }
  };
  return { todo, togglePolling, isPolling: doPoll };
};
Enter fullscreen mode Exit fullscreen mode

The return object contains the fetched to-dos and allows us to pause and continue polling:

// signature
const useFetchTodoWithPolling: (pollingInterval: number) => {
    todo: Ref<Todo | null>;
    togglePolling: () => void;
    isPolling: Ref<boolean>;
}
Enter fullscreen mode Exit fullscreen mode

The following code snippets demonstrate a test that asserts the re-fetching functionality works as expected after five seconds and stops when togglePolling is called:

// useFetchTodoWithPolling.test.ts
describe("useFetchTodoWithPolling", () => {
  const todo = {
    id: 1,
    todo: "vitest",
    completed: false,
    userId: 1,
  };
  vi.mock("./useFetch");
  const useFetchMocked = vi.mocked(useFetch);
  beforeEach(() => {
    // clear the mock to avoid side effects and start count with 0
    vi.clearAllMocks();
  });
  beforeAll(() => {
    vi.useFakeTimers();
  });
  afterAll(() => {
    vi.useRealTimers();
  });
  it("should fetch the API every 5 seconds until polling is stopped", async () => {
    useFetchMocked
      .mockResolvedValueOnce({
        isLoading: false,
        hasError: false,
        error: null,
        data: todo,
      })
      .mockResolvedValueOnce({
        isLoading: false,
        hasError: false,
        error: null,
        data: { ...todo, todo: "rules" },
      });
    const response = useFetchTodoWithPolling(5000);
    await flushPromises();
    expect(response.todo.value?.todo).toEqual("vitest");
    await vi.advanceTimersByTimeAsync(50);
    expect(response.todo.value?.todo).toEqual("vitest");
    await vi.advanceTimersByTimeAsync(4970);
    expect(response.todo.value?.todo).toEqual("rules");
    expect(useFetchMocked).toHaveBeenCalledTimes(2);
    response.togglePolling();
    expect(useFetchMocked).toHaveBeenCalledTimes(2);
  });
Enter fullscreen mode Exit fullscreen mode

Let's discuss what the arrange and act phases have to look like. Regarding arranging the test setup, we need to mock useFetch because this external module is used to fetch to-dos. We saw this in the previous section.

Another important aspect is how to handle the interval. Therefore, we utilize the Vitest API vi.useFakeTimers to work with fake timers in tests. Because we need fake timers for all tests in this test file, we put the code in a beforeAll Hook. It's also good practice to reset to real timers after all tests of the test suite and not rely on implicit resetting:

  beforeEach(() => {
    // clear the mock to avoid side effects and start the count with 0 for every test
    vi.clearAllMocks();
  });
  beforeAll(() => {
    vi.useFakeTimers();
  });
  afterAll(() => {
    vi.useRealTimers();
  });
Enter fullscreen mode Exit fullscreen mode

Clearing all mocks in beforeEach is also good practice to start every mock call count by 0 for every new test. In the end, tests are more semantic, easier to read, and less prone to errors.

Next, as part of the acting phase, we’ll invoke our test candidate:

const response = useFetchTodoWithPolling(5000);
Enter fullscreen mode Exit fullscreen mode

The asserting phase is a bit more complex. Let's break it down:

// the fetch function is called immediately
await flushPromises();
expect(response.todo.value?.todo).toEqual("vitest");
// timer hasn't advanced enough yet
await vi.advanceTimersByTimeAsync(50);
expect(response.todo.value?.todo).toEqual("vitest");
// timer has advanced more than 5 seconds now
await vi.advanceTimersByTimeAsync(4970);
expect(response.todo.value?.todo).toEqual("rules");
expect(useFetchMocked).toHaveBeenCalledTimes(2);
// stop polling
response.togglePolling();
// no fetching should happen now
expect(useFetchMocked).toHaveBeenCalledTimes(2);
Enter fullscreen mode Exit fullscreen mode

flushPromises is required because the composable makes a fetch call once before any timer interval starts. Then we utilize another Vitest utility with vi.advanceTimersByTimeAsync. As you can see with the inline comments, now we establish different timer states and evaluate whether our fetch mock (useFetchMocked) has been called or not.

Then we "act" again and stop polling (response.togglePolling()). Afterward, we evaluate one last time that no more fetch calls have been done.

Vitest cheat sheet

This section will provide examples of various Vitest use cases that you can use for future reference. Unlike the detailed discussion above, the focus of this section is to show how to use Vitest’s API through simplified code snippets.

Testing problems often have multiple solutions. The following examples will cover specific scenarios, such as mocking default imports, which can be challenging, especially for new developers. However, experienced developers familiar with other testing libraries like Jest will also find this compilation useful.

Default imports

The following examples demonstrate how to create test doubles of modules exposed as default exports:

// default-func.ts
const getWithEmoji = (message: string) => {
  return `${message} 😎`;
};
export default getWithEmoji;
// default-obj.ts
export default {
  getWithEmoji: (message: string) => {
    return `${message} 😎`;
  },
};
// default-import.spec.ts
import { type Mock } from "vitest";
import getWithEmojiFunc from "./default-func";
import getWithEmojiObj from "./default-obj";
it("mock default function", () => {
  vi.mock("./default-func", () => {
    return {
      default: vi.fn((message: string) => `${message} 🥳`),
    };
  });
  expect(getWithEmojiFunc("hello world")).toEqual("hello world 🥳");
});
it("spy on default object's method", () => {
  const getWithEmojiSpy = vi.spyOn(getWithEmojiObj, "getWithEmoji") as Mock;
  const result = getWithEmojiSpy("spy kids");
  expect(result).toEqual("spy kids 😎");
  expect(getWithEmojiSpy).toHaveBeenCalledWith("spy kids");
});
Enter fullscreen mode Exit fullscreen mode

The following shows how to create spies of a function exposed as a default import:

// spy-default-func.spec.ts
import { type Mock } from "vitest";
import * as exports from "./default-func";
it("spy on default function", () => {
  const getWithEmojiSpy = vi.spyOn(exports, "default") as Mock;
  const result = getWithEmojiSpy("spy kids");
  expect(result).toEqual("spy kids 😎");
  expect(getWithEmojiSpy).toHaveBeenCalledWith("spy kids");
});
Enter fullscreen mode Exit fullscreen mode

Named imports

The next example shows how to create a spy of a function and a property, both exposed as named imports:

// named-import-property.ts
export const magicNumber: number = 42;
// named-import-property.spec.ts
import * as exports from "./named-import-property";
it("mock property", () => {
  vi.spyOn(exports, "magicNumber", "get").mockReturnValue(41);
  expect(exports.magicNumber).toBe(41);
});
Enter fullscreen mode Exit fullscreen mode

This next example demonstrates how to mock named imports:

// named-import-func.ts
export const getWithEmoji = (message: string) => {
  return `${message} 😎`;
};
// named-import-func.spec.ts
import { getWithEmoji } from "./named-import-func";
it("mock named import (function)", () => {
  vi.mock("./named-import-func");
  const dummyMessage = "Hello, world!";
  const mockGetWithEmji = vi
    .mocked(getWithEmoji)
    .mockReturnValue(`${dummyMessage} 🤩`);
  const result = mockGetWithEmji(dummyMessage);
  expect(result).toBe(`${dummyMessage} 🤩`);
});
Enter fullscreen mode Exit fullscreen mode

Classes and prototypes

These examples demonstrate how to mock imported class modules (named and default imports):

// default-class.ts
export default class Bike {
  ride() {
    return "original value";
  }
}
// named-class.ts
export class Car {
  drive() {
    return "original value";
  }
}
// class.spec.ts
import Bike from "./default-class";
import { Car } from "./named-class";
test("mock a method of a default import class", () => {
  vi.mock("./default-class", () => {
    const MyClass = vi.fn();
    MyClass.prototype.ride = vi.fn();
    return { default: MyClass };
  });
  const myMethodMock = vi.mocked(Bike.prototype.ride);
  myMethodMock
    .mockReturnValueOnce("mocked value")
    .mockReturnValueOnce("another mocked value");
  const myInstance = new Bike();
  let result = myInstance.ride();
  expect(result).toBe("mocked value");
  result = Bike.prototype.ride();
  expect(result).toBe("another mocked value");
  expect(myMethodMock).toHaveBeenCalledTimes(2);
});
test("mock a method of a named export class", () => {
  vi.mock("./named-class", () => {
    const MyClass = vi.fn();
    MyClass.prototype.drive = vi.fn();
    return { Car: MyClass };
  });
  const myMethodMock = vi.mocked(Car.prototype.drive);
  myMethodMock
    .mockReturnValueOnce("mocked value")
    .mockReturnValueOnce("another mocked value");
  const myInstance = new Car();
  let result = myInstance.drive();
  expect(result).toBe("mocked value");
  result = Car.prototype.drive();
  expect(result).toBe("another mocked value");
  expect(myMethodMock).toHaveBeenCalledTimes(2);
});
Enter fullscreen mode Exit fullscreen mode

Snapshot testing

Snapshot testing can be used for data objects. When a snapshot mismatch occurs and causes a test to fail, and if the mismatch is expected, you can press the U key to update the snapshot once:

// data-snapshot.spec.ts
test("snapshot testing", () => {
  const data = {
    id: 1,
    name: "John Doe",
    email: "john.doe@example.com",
  };
  expect(data).toMatchSnapshot();
});
// Vitest snapshot example with an object like above but also a mocked function
test("snapshot testing with a mocked function", () => {
  const person = {
    id: 1,
    name: "John Doe",
    email: "john.doe@example.com",
    contact: vi.fn(),
  };
  expect(person).toMatchSnapshot();
});
Enter fullscreen mode Exit fullscreen mode

This example shows how to use snapshot testing to record the render output of Vue components. Consider the following component:

<!-- AwesomeComponent.vue -->
<template>
  <h1 id="awesome-component">{{ greeting }}</h1>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ name: string }>();
const greeting = computed(() => "Hello, " + props.name);
</script>
<style scoped>
#awesome-component {
  color: red;
}
</style>
Enter fullscreen mode Exit fullscreen mode

The following demonstrates how to use Vue Test Utils to initialize the Vue component with a prop and create an HTML snapshot:

// component-snapshot.spec.ts
import { mount } from "@vue/test-utils";
import AwesomeComponent from "./AwesomeComponent.vue";
test("renders component correctly", () => {
  const wrapper = mount(AwesomeComponent, {
    props: {
      name: "reader",
    },
  });
  expect(wrapper.html()).toMatchSnapshot();
});
Enter fullscreen mode Exit fullscreen mode

Composables and the Composition API

This example demonstrates how to test composables using the Composition API. To trigger an effect (because of watchEffect), you can make use of Vue's nextTick API:

// composition-api.spec.ts
import { ref, watchEffect, nextTick } from "vue";
export function useCounter() {
  const count = ref(2);
  watchEffect(() => {
    if (count.value > 5) {
      count.value = 0;
    }
  });
  const increment = () => {
    count.value++;
  };
  return {
    count,
    increment,
  };
}
test("useCounter", async () => {
  const { count, increment } = useCounter();
  expect(count.value).toBe(2);
  increment();
  expect(count.value).toBe(3);
  increment();
  expect(count.value).toBe(4);
  increment();
  expect(count.value).toBe(5);
  increment();
  expect(count.value).toBe(6);
  // wait for the watcher to update the count
  await nextTick();
  expect(count.value).toBe(0);
});
Enter fullscreen mode Exit fullscreen mode

Vue lifecycle hooks and the Composition API

The following example shows a composable (useCounter) using the Composition API (ref, watchEffect) and a lifecycle hook (onMounted). The Vue component utilizes the composable's interface to update a counter on button clicks:

// useCounter.ts
import { onMounted, ref, watchEffect } from "vue";
export function useCounter() {
  const count = ref(0);
  onMounted(() => {
    count.value = 2;
  });
  watchEffect(() => {
    if (count.value > 3) {
      count.value = 0;
    }
  });
  const increment = () => {
    count.value++;
  };
  return {
    count,
    increment,
  };
}
Enter fullscreen mode Exit fullscreen mode
<!-- Counter.vue -->
<template>
  <div>count: {{ count }}</div>
  <button @click="increment">Increment</button>
</template>
<script setup lang="ts">
import { useCounter } from "./useCounter";
const { count, increment } = useCounter();
</script>
Enter fullscreen mode Exit fullscreen mode

The following snippet shows how to mount a component, fire button click events, and verify that the used composable works as expected:

// Counter.spec.ts
import { mount } from "@vue/test-utils";
import Counter from "./Counter.vue";
import { nextTick } from "vue";
test("renders component correctly", async () => {
  const wrapper = mount(Counter);
  const div = wrapper.find("div");
  expect(div.text()).toContain("count: 0");
  // make sure onMounted() is called by waiting until the next DOM update
  await nextTick();
  expect(div.text()).toContain("count: 2");
  const button = wrapper.find("button");
  button.trigger("click");
  button.trigger("click");
  await nextTick();
  expect(div.text()).toContain("count: 0");
});
Enter fullscreen mode Exit fullscreen mode

Partly mocked modules

The next example demonstrates how to mock only the relevant parts of an external module required for a test scenario (getWithEmoji function of stringOperations module). The other function (log) is irrelevant. Verification of the mock reveals that log is therefore not defined:

// partly-mock-module.ts
const getWithEmoji = (message: string) => {
  return `${message} 😎`;
};
export const stringOperations = {
  log: (message: string) => console.log(message),
  getWithEmoji,
};
// partly-mock-module.spec.ts
import { stringOperations } from "./partly-mock-module";
it("mock method of imported object", () => {
  vi.mock("./partly-mock-module", () => {
    return {
      stringOperations: {
        getWithEmoji: vi.fn().mockReturnValue("Hello world 🤩"),
      },
    };
  });
  const mockGetWithEmoji = vi.mocked(stringOperations.getWithEmoji);
  const result = mockGetWithEmoji("Hello world");
  expect(mockGetWithEmoji).toHaveBeenCalledWith("Hello world");
  expect(result).toEqual("Hello world 🤩");
  expect(vi.isMockFunction(mockGetWithEmoji)).toBe(true);
  expect(stringOperations.log).not.toBeDefined();
});
Enter fullscreen mode Exit fullscreen mode

This is an alternative variant providing both functions of stringOperations. The function getWithEmoji gets replaced by a mock implementation and log is kept in its original form:

// partly-mock-module-restore-original.spec.ts
import { stringOperations } from "./partly-mock-module";
it("mock method of imported object and restore other original properties", () => {
  vi.mock("./partly-mock-module", async (importOriginal) => {
    const original =
      (await importOriginal()) as typeof import("./partly-mock-module");
    return {
      stringOperations: {
        log: original.stringOperations.log,
        getWithEmoji: vi.fn().mockReturnValue("Hello world 🤩"),
      },
    };
  });
  const { getWithEmoji: mockGetWithEmoji, log } = vi.mocked(stringOperations);
  const result = mockGetWithEmoji("Hello world");
  expect(mockGetWithEmoji).toHaveBeenCalledWith("Hello world");
  expect(result).toEqual("Hello world 🤩");
  expect(vi.isMockFunction(mockGetWithEmoji)).toBe(true);
  expect(log).toBeDefined();
  expect(vi.isMockFunction(log)).toBe(false);
});
Enter fullscreen mode Exit fullscreen mode

Access to external variables within mock implementations

In contrast to vi.mock, vi.doMock isn't hoisted to the top of a file. It's useful when you need to incorporate external variables inside mock implementations:

// someModule.ts
export function someFunction() {
  return "original implementation";
}
// doMock.spec.ts
import { someFunction } from "./someModule";
it("original module", () => {
  const result = someFunction();
  expect(result).toEqual("original implementation");
});
it("doMock allows to use variables from scope", async () => {
  const dummyText = "dummy text";
  // vi.mock does not allow to use variables from the scope. It leads to errors like:
  // Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file.
  // vi.doMock does not get hoisted to the top instead of vi.mock
  vi.doMock("./someModule", () => {
    return {
      someFunction: vi.fn().mockReturnValue(dummyText),
    };
  });
  // dynamic import is required to get the mocked module with vi.docMock
  const { someFunction: someFunctionMock } = await import("./someModule");
  const result = someFunctionMock();
  expect(someFunctionMock).toHaveBeenCalled();
  expect(result).toEqual(dummyText);
});
Enter fullscreen mode Exit fullscreen mode

Clean up test doubles

The following examples highlight how to use mockClear, mockReset, and mockRestore to clean up your tests, especially by testing doubles to avoid side effects:

// add.ts
export const add = (a, b) => a + b;
// cleanup-mocks.spec.ts
import { add } from "./add";
test("mockClear", () => {
  const mockFunc = vi.fn();
  mockFunc();
  expect(mockFunc).toHaveBeenCalledTimes(1);
  // resets call history
  mockFunc.mockClear();
  mockFunc();
  expect(mockFunc).toHaveBeenCalledTimes(1);
});
test("mockReset vs mockRestore", async () => {
  const mockAdd = vi.fn(add).mockImplementation((a, b) => 2 * a + 2 * b);
  expect(vi.isMockFunction(mockAdd)).toBe(true);
  expect(mockAdd(1, 1)).toBe(4);
  expect(mockAdd).toHaveBeenCalledTimes(1);
  // resets call history and mock function returns undefined
  mockAdd.mockReset();
  expect(vi.isMockFunction(mockAdd)).toBe(true);
  expect(mockAdd(1, 1)).toBeUndefined();
  expect(mockAdd).toHaveBeenCalledTimes(1);
  // resets call history and mock function restores implementation to add
  mockAdd.mockRestore();
  expect(vi.isMockFunction(mockAdd)).toBe(true);
  expect(mockAdd(1, 1)).toBe(2); // original implementation
  expect(mockAdd).toHaveBeenCalledTimes(1);
});
Enter fullscreen mode Exit fullscreen mode

Auto-mocking modules and global mocks

Storing test doubles in a dedicated folder (__mocks__) allows for automatic mocking. The following example shows how to mock axios globally:

// <root-folder>/__mocks__/axios.ts
import { vi } from "vitest";
const mockAxios = {
  get: vi.fn((url: string) =>
    Promise.resolve({ data: { urlCharCount: url.length } }),
  ),
  post: vi.fn(() => Promise.resolve({ data: {} })),
  // Add other methods as needed
};
export default mockAxios;
// axios.auto-mocking.spec.ts
import axios from "axios";
vi.mock("axios");
// auto-mocking example with <root-folder>/__mocks__ folder
test("mocked axios", async () => {
  const response = await axios.get("url");
  expect(response.data.urlCharCount).toBe(3);
  expect(axios.get).toHaveBeenCalledWith("url");
  expect(axios.delete).toBeUndefined();
  expect(vi.isMockFunction(axios.get)).toBe(true);
  expect(vi.isMockFunction(axios.post)).toBe(true);
  expect(vi.isMockFunction(axios.delete)).toBe(false);
});
// use actual axios in test
test("can get actual axios", async () => {
  const ax = await vi.importActual<typeof axios>("axios");
  expect(vi.isMockFunction(ax.get)).toBe(false);
});
Enter fullscreen mode Exit fullscreen mode

The following example demonstrates how you can mock axios only for individual tests:

// axios.spec.ts
import axios from "axios";
test("mocked axios", async () => {
  const { default: ax } =
    await vi.importMock<typeof import("axios")>("axios");
  const response = await ax.get("url");
  expect(ax.get).toHaveBeenCalledWith("url");
  expect(response.data.urlCharCount).toEqual(3);
});
test("actual axios is not mocked", async () => {
  expect(vi.isMockFunction(axios.get)).toBe(false);
});
Enter fullscreen mode Exit fullscreen mode

Date and time

Vitest provides the vi.setSystemTime method to fake the system time:

// date-and-time.spec.ts
const getCurrentTime = () => new Date().toTimeString().slice(0, 5);
it("should return the correct system time", () => {
  vi.setSystemTime(new Date("2024-04-04 15:17"));
  expect(getCurrentTime()).toBe("15:17");
  // cleanup
  vi.useRealTimers();
});
Enter fullscreen mode Exit fullscreen mode

Rejected promises and errors

The following examples show how to verify thrown exceptions:

// error.spec.ts
test("should throw an error", () => {
  expect(() => {
    throw new Error("Error message");
  }).toThrow("Error message");
});
test("should throw an error after rejected fetch", async () => {
  const errorMessage = "Network error";
  vi.stubGlobal(
    "fetch",
    vi.fn(() => Promise.reject(new Error(errorMessage))),
  );
  await expect(fetch("https://api.example.com/data")).rejects.toThrow(
    "Network error",
  );
});
test("more sophisticated example of expecting error and resolved value", async () => {
  const errorMessage = "Network error";
  class MyError extends Error {
    constructor(message: string) {
      super(message);
      this.name = "MyError";
    }
  }
  globalThis.fetch = vi.fn().mockRejectedValue(new MyError(errorMessage));
  await expect(fetch("https://api.example.com/data")).rejects.toThrowError(
    "Network error",
  );
  await expect(fetch("https://api.example.com/data")).rejects.toThrowError(
    Error,
  );
  await expect(fetch("https://api.example.com/data")).rejects.toThrowError(
    /Network/,
  );
  globalThis.fetch = vi.fn().mockResolvedValue("success");
  const response = await fetch("https://api.example.com/data");
  expect(response).toBe("success");
});
Enter fullscreen mode Exit fullscreen mode

Replace global fetch

The following shows different methods for replacing global fetch with test doubles:

// fetch.spec.ts
test("variant with globalThis.fetch", async () => {
  const dummyData = { message: "hey" };
  const mockResponse = {
    ok: true,
    statusText: "OK",
    json: async () => dummyData,
  } as Response;
  globalThis.fetch = vi.fn().mockResolvedValue(mockResponse);
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  expect(data).toEqual(dummyData);
});
test("variant with globalThis.fetch and vi.mocked", async () => {
  const dummyBlob = new Blob();
  const mockResponse = {
    ok: true,
    statusText: "OK",
    blob: async () => dummyBlob,
  } as Response;
  globalThis.fetch = vi.fn();
  vi.mocked(fetch).mockResolvedValue(mockResponse);
  const response = await fetch("https://api.example.com/data");
  const data = await response.blob();
  expect(response.blob).toBeDefined();
  expect(data).toEqual(dummyBlob);
  expect(response.json).not.toBeDefined();
});
test("variant with vi.stubGlobal", async () => {
  const dummyData = { data: "hey" };
  const dummyBlob = new Blob();
  vi.stubGlobal(
    "fetch",
    vi.fn(() =>
      Promise.resolve({
        blob: async () => dummyBlob,
        json: () => dummyData,
      }),
    ),
  );
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  const blob = await response.blob();
  expect(response).toEqual({
    json: expect.any(Function),
    blob: expect.any(Function),
  });
  expect(data).toEqual(dummyData);
  expect(blob).toEqual(dummyBlob);
});
test.todo("variant with rejected fetch", async () => {
  // see error.spec.ts
});
Enter fullscreen mode Exit fullscreen mode

Parameterize tests

With Vitest's test.each API, you can pass a different set of data into tests:

// run tests 3x with different string values
const inputs = ["Hello", "world", "!"];
test.each(inputs)("Testing string length", (input) => {
  expect(input.length).toBeGreaterThan(0);
});
test.each(inputs)("Testing string lengths of %s", (input) => {
  expect(input.length).toBeGreaterThan(0);
});
Enter fullscreen mode Exit fullscreen mode

Stub globalThis and window with stubGlobal

vi.stubGlobal can be used to mock different global properties such as console.log or fetch. To stub window properties, you need to use jsdom or happy-dom:

test("Math example", () => {
  vi.stubGlobal("Math", { random: () => 0.5 });
  const result = Math.random();
  expect(result).toBe(0.5);
});
test("Date example", () => {
  vi.stubGlobal(
    "Date",
    class {
      getTime() {
        return 1000;
      }
    },
  );
  expect(new Date().getTime()).toBe(1000);
  expect(new Date().getTime()).not.toBe(2000);
});
test("console example", () => {
  vi.stubGlobal("console", {
    log: vi.fn(),
    error: vi.fn(),
  });
  console.log("Hello, World!");
  console.error("An error occurred!");
  const log = vi.mocked(console.log);
  const error = vi.mocked(console.error);
  expect(log).toHaveBeenCalledWith("Hello, World!");
  expect(error).toHaveBeenCalledWith("An error occurred!");
  expect(vi.isMockFunction(log)).toBe(true);
  expect(vi.isMockFunction(error)).toBe(true);
});
test("window example", () => {
  vi.stubGlobal("window", {
    innerWidth: 1024,
    innerHeight: 768,
  });
  expect(window.innerWidth).toBe(1024);
  expect(window.innerHeight).toBe(768);
});
test.todo("fetch example", () => {
  // see fetch.spec.ts
});
Enter fullscreen mode Exit fullscreen mode

Verification of test doubles

Vitest features many useful API functions to verify invocations of test doubles:

test("verify return value of a mocked function", () => {
  const mockFn = vi.fn();
  // Set the return value of the mock function
  mockFn.mockReturnValue("mocked value");
  // Call the mock function
  const result = mockFn();
  // Verify that the return value has a particular value
  expect(result).toBe("mocked value");
  // Verify that the return value is of a particular type
  expect(typeof result).toBe("string");
});
test("verify invocations of a mocked function with any argument", () => {
  const mockFn = vi.fn();
  // Call the mock function with different arguments
  mockFn("arg1", 123);
  mockFn("arg2", { key: "value" });
  // Verify that the mock function was called with any string as the first argument
  expect(mockFn).toHaveBeenCalledWith(expect.any(String), expect.anything());
  // Verify that the mock function was called with any number as the second argument
  expect(mockFn).toHaveBeenCalledWith(expect.anything(), expect.any(Number));
  // Verify that the mock function was called with any object as the second argument
  expect(mockFn).toHaveBeenCalledWith(expect.anything(), expect.any(Object));
});
test("verify invocations of a mocked function with calls array", () => {
  const mockFn = vi.fn();
  // Call the mock function with different arguments
  mockFn("arg1", "arg2");
  mockFn("arg3", "arg4");
  // Verify that the mock function was called
  expect(mockFn).toHaveBeenCalled();
  // Verify that the mock function was called exactly twice
  expect(mockFn).toHaveBeenCalledTimes(2);
  // Verify that the mock function was called with specific arguments
  expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
  expect(mockFn).toHaveBeenCalledWith("arg3", "arg4");
  // Verify the order of calls and their arguments using the mock array
  expect(mockFn.mock.calls[0]).toEqual(["arg1", "arg2"]);
  expect(mockFn.mock.calls[1]).toEqual(["arg3", "arg4"]);
  // clear the mock call history
  mockFn.mockClear();
  // Verify that the mock function was not called after resetting
  expect(mockFn).not.toHaveBeenCalled();
  // Call the mock function again with different arguments
  mockFn("arg5", "arg6");
  // Verify that the mock function was called once after resetting
  expect(mockFn).toHaveBeenCalledTimes(1);
  // Verify that the mock function was called with specific arguments after resetting
  expect(mockFn).toHaveBeenCalledWith("arg5", "arg6");
  // Verify the order of calls and their arguments using the mock array after resetting
  expect(mockFn.mock.calls[0]).toEqual(["arg5", "arg6"]);
});
test("verify invocations of a mocked function with a specific object", () => {
  interface MyInterface {
    key: string;
  }
  const mockFn = vi.fn();
  // Call the mock function with an object that matches the MyInterface interface
  mockFn({ key: "value", extraProp: "extra" });
  // Verify that the mock function was called with an object containing a specific key-value pair
  expect(mockFn).toHaveBeenCalledWith(
    expect.objectContaining({ key: "value" } as MyInterface),
  );
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

While testing may require an upfront investment of time and effort, the long-term benefits it provides in terms of code quality, maintainability, and developer understanding make it an invaluable practice in software development. This is why you should invest your time in learning Vitest to improve the quality of your JavaScript project.


Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps — start monitoring for free.

💖 💪 🙅 🚩
leemeganj
Megan Lee

Posted on June 11, 2024

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

Sign up to receive the latest update from our blog.

Related