EmilianoAugust 16, 2024

Testing a Pinia Store in Nuxt 3 Using Vitest

To ensure reliability in Vue.js applications, it is essential to implement effective testing. In this article, we will explore how to build a Pinia store that interacts with an external API and how to write tests to validate its behavior and error handling.

Creating the Pinia store

First, we define a Pinia store that handles data from an API. Here is an example where we store information retrieved from an external API:

import { defineStore } from "pinia";

export const useDataStore = defineStore({
    id: "data",
    state: () => ({
        data: null,
        error: {
            message: "",
            status: false
        }
    }),
    actions: {
        async fetchData(apiUrl: string) {
            try {
                if (this.data) {
                    return;
                }
                const { data } = await useAsyncData("data", () =>
                    $fetch(apiUrl)
                );
                if (data.value) {
                    this.data = data.value;
                }
            } catch (error) {
                this.error.status = true;
                this.error.message = "Error fetching data";
            }
        }
    }
});

In this store, the fetchData function makes a request to an external API and stores the returned data in state.

Mocking useAsyncData in tests

For testing, we use Vitest and create a mock for useAsyncData, the function that performs the API request. Here is how to configure the tests:

import { cleanup } from "@testing-library/vue";
import { vi, describe, it, expect, afterEach, beforeEach } from "vitest";
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
import { setActivePinia, createPinia } from "pinia";
import { useDataStore } from "@/stores/data";

// Mock for useAsyncData
const useAsyncDataMock = vi.hoisted(() => vi.fn());
mockNuxtImport("useAsyncData", () => useAsyncDataMock);

const mockApiData = {
    id: 1,
    name: "Example",
    detail: "This is an example"
};

// Mock for successful data
const setupSuccessMock = () => {
    useAsyncDataMock.mockImplementation(() => {
        return {
            data: ref(mockApiData),
            error: ref(null),
        };
    });
};

afterEach(() => {
    cleanup();
    useAsyncDataMock.mockClear();
});

describe("Data Store", () => {
    beforeEach(() => {
        setActivePinia(createPinia());
    });

    describe("when data is retrieved successfully", () => {
        it("should set the data in the store", async () => {
            const store = useDataStore();
            setupSuccessMock();
            await store.fetchData("https://api.example.com/data");

            expect(store.data).toEqual(mockApiData);
        });
    });

    describe("when data has already been fetched", () => {
        it("should not make the request again", async () => {
            const store = useDataStore();
            setupSuccessMock();
            await store.fetchData("https://api.example.com/data");
            await store.fetchData("https://api.example.com/data");

            expect(useAsyncDataMock).toHaveBeenCalledTimes(1);
        });
    });

    describe("when the request fails", () => {
        it("should set an error message", async () => {
            const store = useDataStore();
            useAsyncDataMock.mockImplementation(() => {
                throw new Error("Error fetching data");
            });
            await store.fetchData("https://api.example.com/data");

            expect(store.error.status).toBe(true);
            expect(store.error.message).toBe("Error fetching data");
        });
    });
});

Test setup and analysis

Below, we break down how the key tests for this store work:

Successful data mock: We use setupSuccessMock to simulate a successful response from the external API, ensuring that the store saves the data correctly.

const setupSuccessMock = () => {
    useAsyncDataMock.mockImplementation(() => {
        return {
            data: ref(mockApiData),
            error: ref(null),
        };
    });
};

Data storage verification: We test that the retrieved data is stored in the store after a successful request. If the request has already been made previously, we verify that it is not made again.

describe("when data is retrieved successfully", () => {
    it("should set the data in the store", async () => {
        const store = useDataStore();
        setupSuccessMock();
        await store.fetchData("https://api.example.com/data");

        expect(store.data).toEqual(mockApiData);
    });
});

Error handling: A request failure is simulated to verify that the store updates its error state correctly.

describe("when the request fails", () => {
    it("should set an error message", async () => {
        const store = useDataStore();
        useAsyncDataMock.mockImplementation(() => {
            throw new Error("Error fetching data");
        });
        await store.fetchData("https://api.example.com/data");

        expect(store.error.status).toBe(true);
        expect(store.error.message).toBe("Error fetching data");
    });
});

With this setup, you can ensure that your store handles API data and errors correctly, which is essential for maintaining the integrity and reliability of applications that depend on external data.