docs
  1. SCAYLE Resource Center
  2. Developer Guide
  3. Testing
  4. Unit testing

Unit testing

As technical complexity rises, it opens the door for potential side effects or even bugs. It is therefore important to verify and test critical code. To help with this, Storefront Boilerplate comes with a range of preconfigured unit test setups and an ever growing test suite.

The unit test coverage of the Storefront Boilerplate is getting extended with every release. The current focus hereby lies on functional testing for composables and utilities. Vue component testing (integration testing) is not yet within the scope, but will be added at a later point.

Getting started

By leveraging the unit testing framework vitest, we can isolate and test individual functions, components, and logic within our Storefront Boilerplate codebase. This granular approach allows us to verify expected behavior, identify and address bugs early in the development lifecycle, and refactor with confidence. Ultimately, a robust suite of unit tests contributes significantly to the overall stability, maintainability, and reliability of our codebase, ensuring a smoother development experience for everyone involved.

We recommend getting familiar with the testing capabilities of Vitest and Nuxt first, before proceeding with this guide:

Setup

Storefront Boilerplate relies on vitest as an integrated unit test framework.
To extend the testing capabilities, Storefront Boilerplate also relies on @vue/test-utils, @nuxt/test-utils and happy-dom.

These dependencies allow to cover the majority of potential unit test cases in the context of a Vue and Nuxt application.

@vue/test-utils

@vue/test-utils is a library for testing Vue.js components. It provides a way to mount and interact with Vue components in a simulated browser environment, allowing to:

  • Mount components: You can mount individual components in isolation or as part of a larger component tree.
  • Interact with the component: Trigger events, update props, and manipulate the component's state.
  • Assert on the output: Make assertions about the rendered HTML, the component's data, computed properties, and more.
  • Choose Your Rendering Method: Select from different rendering methods like shallowMount, mount, or render based on the depth of rendering and testing needs.
  • Work with Slots: Test how your component manages content passed through slots and assert on their rendered output.
  • Simplify Asynchronous Operations: Conveniently work with asynchronous actions like API calls or transitions using async/await and utilities like nextTick.
  • Access Component Instance: Directly access the underlying Vue instance of the mounted component to test lifecycle hooks, methods, or internal state changes.
  • Mock Dependencies: Easily replace external dependencies like Vuex stores or API services with mocks to isolate component logic and create controlled testing scenarios.
  • Improve Test Readability: Write cleaner and more focused tests with helper methods for finding elements, triggering events, and making assertions.

Essentially, it provides the tools you need to write comprehensive unit and integration tests for your Vue components, ensuring they function correctly and meet the desired requirements.

Simple Example Test:

// MyStorefrontComponent.test.ts
import { shallowMount } from '@vue/test-utils';
import MyComponent from './MyStorefrontComponent.vue';

describe('MyStorefrontComponent.vue', () => {
  it('renders a message', () => {
    const wrapper = shallowMount(MyComponent, {
      propsData: {
        message: 'Hello World'
      }
    });

    expect(wrapper.text()).toContain('Hello World'); 
  });
});

This test case uses shallowMount to render MyComponent without its child components. It then checks that the rendered output contains the text "Hello World", which is passed as a prop to the component.

@nuxt/test-utils

@nuxt/test-utils is a library specifically designed for testing Nuxt.js applications. Building upon @vue/test-utils, it provides additional functionalities and utilities tailored to Nuxt's unique features:

  • Mount Nuxt Components: Similar to @vue/test-utils, you can mount individual components, but @nuxt/test-utils ensures that Nuxt plugins, middleware, and other configurations are correctly applied, mimicking a real Nuxt environment.
  • Test Server-Side Rendering (SSR): You can test the server-side rendering behavior of your Nuxt components and pages, ensuring the correct data is pre-fetched and rendered on the server before being sent to the client.
  • Interact with the Nuxt Context: Access and interact with the Nuxt context ($nuxt) within your tests. This allows you to test functionalities like programmatically navigating between pages, accessing route data, or interacting with the store.
  • Mock Nuxt Modules: Easily mock the behavior of Nuxt modules within your test environment. This is useful for isolating specific functionalities or testing different module configurations without affecting other parts of your application.
  • Handle Asynchronous Data Fetching: Test components and pages that rely on asynchronous data fetching methods like asyncData, fetch, or nuxtServerInit and assert that data is correctly fetched and rendered.

Simple Example Test:

// MyStorefrontComponent.nuxt.test.ts
import { mount } from '@nuxt/test-utils';
import MyComponent from '~/components/MyStorefrontComponent.vue';

describe('MyStorefrontComponent', () => {
  it('displays data from the store', async () => {
    const wrapper = await mount(MyComponent, {
      // ...additional options to configure the Nuxt environment
    });

    // Access the store from the wrapper and make assertions
    expect(wrapper.vm.$store.state.myData).toBe('Expected Data'); 
  });
});

the @nuxt/test-utils is able to let a test run in a Nuxt application-specific environment. This enables the test to access many of the abstracted Nuxt utilities and functionalities without the need to explicitly mock them manually. For this to work, a test needs to have the *.nuxt.test.ts suffix.

In essence, @nuxt/test-utils streamlines the testing process for Nuxt applications, providing the tools to ensure your components, pages, and server-side rendering logic all work together seamlessly.

happy-dom

happy-dom, as a lightweight and fast DOM implementation, pairs well with vitest, a blazingly fast unit testing framework powered by vite. Here's how they work together for efficient and streamlined DOM testing:

Why happy-dom with vitest?

  • Speed and Efficiency: Both tools prioritize speed. Vitest utilizes native ES modules and efficient dependency handling, while happy-dom's lightweight design minimizes DOM operation overhead. This combination results in significantly faster test execution times.
  • Fine-grained Control: You have direct access to DOM APIs, providing precise control over component rendering, manipulation, and assertions. This is beneficial when you need very specific control or prefer not to abstract away DOM interactions.
  • Minimal Dependencies: You can write tests without relying on additional testing library abstractions, keeping your project dependencies lean and potentially simplifying your setup.

Simple Example Setup:

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom', // Use happy-dom as the DOM environment
    setupFiles: ['./vitest.setup.js'], // Add a setup file (optional)
  },
});
// vitest.setup.js (Optional)
import { Window } from 'happy-dom';

// Make 'window' and 'document' globally available.
globalThis.window = new Window();
globalThis.document = window.document;

Simple Example Test:

// MyStorefrontComponent.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import MyComponent from './MyStorefrontComponent.vue';

describe('MyStorefrontComponent', () => {
  let componentContainer; 

  beforeEach(() => {
    // Create a fresh container before each test
    componentContainer = document.createElement('div');
    document.body.appendChild(componentContainer);
  });

  it('displays the correct message', async () => {
    // Mount the Vue component 
    createApp(MyComponent, { message: 'Test Message' }).mount(componentContainer);

    // Wait for the DOM updates
    await nextTick(); 

    expect(componentContainer.querySelector('p').textContent).toBe('Test Message'); 
  });
});

Explanation:

  1. vite.config.js: Set Vitest to use happy-dom. A setup file (vitest.setup.js) is included for global configurations.
  2. vitest.setup.js: Create a global window and document using happy-dom.
  3. MyComponent.test.js:
    • Import from Vitest and your component.
    • Create a container element in beforeEach for each test.
    • Mount your Vue component to the container using createApp.
    • Wait for the next tick of the Vue update cycle using nextTick().
    • Make assertions using querySelector or other DOM APIs.

Important Considerations:

  • Timing Is Crucial: You'll need to manage when Vue updates the DOM. Using await nextTick() is essential to ensure your assertions run after Vue has had a chance to update.
  • More Verbose: You'll write more verbose assertions with DOM APIs directly compared to using testing library abstractions.
  • Vue-Specific Logic: The way you mount/render your Vue component may need to be adjusted based on your component's setup (Options API or Composition API).

While this approach offers fine-grained control and avoids extra dependencies, using a testing library often leads to more robust and maintainable tests. Choose the method that best suits your project's needs and complexity.

Remember that while happy-dom is excellent for unit and integration tests involving DOM manipulation, it's essential to complement these with End-to-End tests in real browsers to cover all aspects of your application.


Writing Storefront Unit Tests

Test File Naming & Location Convention

While writing unit tests within a Storefront application, it's important to keep the context of the test case in mind. Can a certain function be tested independently of its application context, or does it require the application context to work?

For example, a simple string manipulation utility can be tested without an application context, while a custom Vue composable that interacts with the global application state composable requires the Nuxt application context.

To simplify and differentiate between these test cases, the respective test files are executed differently by vitest depending on the file ending suffix:

  • *.test.ts
    • The test file is run in a "basic" encapsulated unit test context
  • *.nuxt.test.ts

All tests should directly be located beside the composable / utility / component they test. This allows for a tighter coupling and increased maintainability.

Testing Composables

To understand how unit tests for composables should ideally be structured, let's dissect a more complex test example like the useFilter composable used for handling filter behaviour on the Product Listing Page:

1. Mocking Dependencies

// useFilter.test.ts
import { describe, it, vi, expect, beforeEach } from 'vitest'
import { type CentAmount } from '@scayle/storefront-nuxt'
import { useFilter } from './useFilter'

const mocks = vi.hoisted(() => {
  return {
    route: { query: {} },
    router: { push: vi.fn() },
    useAppliedFilters: {
      productConditions: { value: '' },
      appliedFilter: { value: {} },
      appliedFiltersCount: { value: 0 },
    },
    useTrackingEvents: {
      trackFilterApply: vi.fn(),
      trackFilterFlyout: vi.fn(),
    },
    useFilters: { data: { value: { filters: {}, unfilteredCount: 0 } } },
    useToast: { show: vi.fn() },
    useI18n: { t: vi.fn().mockImplementation((key) => key) },
  }
})

vi.mock('#app/composables/router', () => ({
  useRoute: vi.fn().mockReturnValue(mocks.route),
  useRouter: vi.fn().mockReturnValue(mocks.router),
}))

vi.mock('#storefront/composables', () => ({
  useFilters: vi.fn().mockReturnValue(mocks.useFilters),
}))

vi.mock('#i18n', () => ({
  useI18n: vi.fn().mockReturnValue(mocks.useI18n),
}))

vi.mock('~/composables', () => ({
  useAppliedFilters: vi.fn().mockReturnValue(mocks.useAppliedFilters),
  useTrackingEvents: vi.fn().mockReturnValue(mocks.useTrackingEvents),
  useToast: vi.fn().mockReturnValue(mocks.useToast),
}))

// ...
  • Mocked Modules: The test begins by mocking external and internal dependencies of useFilter. This includes:
    • #app/composables/router: Mocks useRoute and useRouter for routing interactions.
    • #storefront/composables: Mocks useFilters which presumably provides filter data.
    • #i18n: Mocks useI18n for internationalization.
    • ~/composables: Mocks several other composables (useAppliedFilters, useTrackingEvents, useToast) used within useFilter.
  • Mocked Data: A mocks object centrally holds all the mocked values and functions. This allows easy access and manipulation of mocked behavior throughout the tests.

2. Test Suite: describe('useFilter', ...)

// useFilter.test.ts
// ...
describe('useFilter', () => {
  beforeEach(() => {
    mocks.useFilters.data.value.filters = [
      {
        id: 1,
        slug: 'brand',
        name: 'Brand',
        values: [
          {
            name: 'value',
            id: 2,
            productCount: 10,
            value: 123,
          },
        ],
        type: 'attributes',
      },
      {
        id: 12,
        slug: 'sale',
        name: 'Sale',
        values: [
          {
            name: true,
            productCount: 12,
          },
          {
            name: false,
            productCount: 2,
          },
        ],
        type: 'boolean',
      },
      {
        id: 3,
        slug: 'prices',
        name: 'Prices',
        values: [
          {
            min: 10,
            max: 300,
            productCount: 30,
          },
        ],
        type: 'range',
      },
    ]
    mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
    mocks.useAppliedFilters.appliedFiltersCount.value = 0
    mocks.route.query = {}

    vi.clearAllMocks()
  })

// ...
  • beforeEach Hook: This setup function runs before each individual test (it block). It ensures a clean state by:
    • Resetting mocks.useFilters.data.value.filters with a predefined set of filter data, simulating data fetched from an API or store.
    • Clearing applied filters (useAppliedFilters) and query parameters (route.query).
    • Resetting all mocked functions using vi.clearAllMocks(), guaranteeing that each test starts with a fresh mock setup.

3. Individual Test Cases (it blocks):

  • should have correct filterable values: This test focuses on the availableFilters computed property of useFilter. It checks if the computed property correctly structures and returns the filter data as expected.
  • should filter our boolean filter without products: Tests the filtering logic, particularly for boolean filters with zero product counts, ensuring those filters are excluded.
  • Nested describe blocks: The test suite further nests several describe blocks to group related test cases, enhancing readability and organization. These nested blocks include:
    • applyAttributeFilter: Tests the functionality of applying attribute-based filters, covering various scenarios like adding, merging, and removing filter values in the URL query parameters.
    • applyBooleanFilter: Specifically, tests applying boolean filters, checking if the query parameters are updated correctly.
    • applyPriceFilter: Focuses on testing the application of price range filters to the query parameters.
    • resetFilter: Tests the resetFilter function, which handles removing a specific filter from the applied filters.
    • resetPriceFilter: Similar to resetFilter, but specific to resetting price filters.
    • resetFilters: Tests the resetFilters function for clearing all applied filters.
// useFilter.test.ts
// ...

  it('should have correct filterable values', () => {
    const { availableFilters } = useFilter()

    expect(availableFilters.value).toHaveLength(3)
    expect(availableFilters.value).toStrictEqual([
      {
        id: 1,
        name: 'Brand',
        slug: 'brand',
        type: 'attributes',
        values: [
          {
            id: 2,
            name: 'value',
            productCount: 10,
            value: 123,
          },
        ],
      },
      {
        id: 12,
        name: 'Sale',
        slug: 'sale',
        type: 'boolean',
        values: [
          {
            name: true,
            productCount: 12,
          },
          {
            name: false,
            productCount: 2,
          },
        ],
      },
      {
        id: 3,
        name: 'Prices',
        slug: 'prices',
        type: 'range',
        values: [
          {
            max: 300,
            min: 10,
            productCount: 30,
          },
        ],
      },
    ])
  })

  it('should filter our boolean filter without products', () => {
    mocks.useFilters.data.value.filters = [
      {
        id: 12,
        slug: 'sale',
        name: 'Sale',
        values: [
          {
            name: true,
            productCount: 0,
          },
          {
            name: false,
            productCount: 2,
          },
        ],
        type: 'boolean',
      },
      {
        id: 12,
        slug: 'otherBoolean',
        name: 'other',
        values: [
          {
            name: true,
            productCount: 0,
          },
          {
            name: false,
            productCount: 2,
          },
        ],
        type: 'boolean',
      },
      {
        id: 12,
        slug: 'existingBoolean',
        name: 'Existing',
        values: [
          {
            name: true,
            productCount: 12,
          },
          {
            name: false,
            productCount: 2,
          },
        ],
        type: 'boolean',
      },
    ]

    const { availableFilters } = useFilter()
    expect(availableFilters.value).toHaveLength(1)
    expect(availableFilters.value[0]).toStrictEqual({
      id: 12,
      name: 'Existing',
      slug: 'existingBoolean',
      type: 'boolean',
      values: [
        {
          name: true,
          productCount: 12,
        },
        {
          name: false,
          productCount: 2,
        },
      ],
    })
  })

  describe('applyAttributeFilter', () => {
    it('should add new filter and value to query', () => {
      mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '1',
        },
      })
    })

    it('should add new filter and value to query and merge with existing query', () => {
      mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
      mocks.route.query = {
        'filters[otherAttributes]': '1, 2, 3',
        sort: 'asc',
        term: 'term',
      }
      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '1',
          'filters[otherAttributes]': '1, 2, 3',
          sort: 'asc',
          term: 'term',
        },
      })
    })

    it('should add new value to query', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '2,3,4,1',
        },
      })
    })

    it('should remove existing value from query', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 2)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '3,4',
        },
      })
    })

    it('should remove existing value and filter from query', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [{ key: 'newAttribute', values: [2], type: 'attributes' }],
      }

      mocks.useAppliedFilters.appliedFiltersCount.value = 1

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 2)
      expect(mocks.router.push).toBeCalledWith({ query: {} })
    })

    it('should show toast message on filter applied and modal closed', async () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }

      const { applyAttributeFilter, onSlideInClose } = useFilter()
      await applyAttributeFilter('newAttribute', 1)
      onSlideInClose()
      expect(mocks.useToast.show).toBeCalledWith(
        'filter.updated_notification',
        { type: 'SUCCESS' },
      )
    })

    it('should not show toast message on modal closed', async () => {
      const { onSlideInClose } = useFilter()
      onSlideInClose()
      expect(mocks.useToast.show).not.toBeCalled()
    })
  })

  describe('applyBooleanFilter', () => {
    it('should add boolean filter and value to query if true', () => {
      const { applyBooleanFilter } = useFilter()
      applyBooleanFilter('newBool', true)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newBool]': 'true',
        },
      })
    })

    it('should remove boolean filter and value from query if false', () => {
      mocks.useAppliedFilters.appliedFiltersCount.value = 1
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [{ key: 'newBool', value: true, type: 'boolean' }],
      }

      const { applyBooleanFilter } = useFilter()
      applyBooleanFilter('newBool', false)
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })
  })

  describe('applyPriceFilter', () => {
    it('should apply prices correctly', () => {
      const { applyPriceFilter } = useFilter()
      applyPriceFilter([99 as CentAmount, 2345 as CentAmount])
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[minPrice]': '99',
          'filters[maxPrice]': '2345',
        },
      })
    })
  })

  describe('resetFilter', () => {
    it('should reset correctly', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [1, 2, 3, 4], type: 'attributes' },
        ],
      }
      mocks.useAppliedFilters.appliedFiltersCount.value = 1
      mocks.route.query = {
        'filters[newAttribute]': [1, 2, 3, 4],
      }
      const { resetFilter } = useFilter()
      resetFilter('newAttribute')
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should not reset if slug is not applied', () => {
      const { resetFilter } = useFilter()
      resetFilter('newAttribute')
      expect(mocks.router.push).not.toBeCalledWith({
        query: {},
      })
    })
  })

  describe('resetPriceFilter', () => {
    it('should reset correctly', () => {
      mocks.useAppliedFilters.appliedFiltersCount.value = 1
      mocks.route.query = {
        'filters[minPrice]': '1',
        'filters[maxPrice]': '12',
      }
      const { resetPriceFilter } = useFilter()
      resetPriceFilter()
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should not reset if price is not applied', () => {
      const { resetPriceFilter } = useFilter()
      resetPriceFilter()
      expect(mocks.router.push).not.toBeCalledWith({
        query: {},
      })
    })
  })

  describe('resetFilters', () => {
    it('should reset all correctly', () => {
      mocks.useAppliedFilters.appliedFiltersCount.value = 3
      mocks.route.query = {
        'filters[minPrice]': '1',
        'filters[maxPrice]': '12',
        'filters[attribute]': '[1,2,3,4,]',
        'filters[sale]': true,
      }
      const { resetFilters } = useFilter()
      resetFilters()
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should not reset if no filter is not applied', () => {
      const { resetFilters } = useFilter()
      resetFilters()
      expect(mocks.router.push).not.toBeCalledWith({
        query: {},
      })
    })
  })
})

4. Overall Structure

  • The test suite follows a clear and organized structure using describe and it blocks for better readability.
  • Mocks are effectively used to isolate the useFilter logic, making the tests more deterministic and less reliant on external factors. It's important to note that not every external dependency (e.g. functions) should be mocked, but only those that are needed and influence the execution and / or outcome within the tested functionality.
  • The beforeEach hook ensures a consistent and clean starting state for each test case.
  • The use of nested describe blocks logically groups related tests.
  • Data for assertions should be explicitly defined and used directly within the specific test block and should not be reused across multiple test cases. This allows to understand the test case quicker and avoid potential data mutations.

Testing Components

The unit test coverage of the Storefront Boilerplate is getting extended with every release. The current focus hereby lies on functional testing for composables and utilities. Vue component testing (integration testing) is not yet within the scope, but will be added at a later point.


Best Practices

  1. Focused Tests: One aspect per test case
  2. Clarity and Conciseness: Meaningful names, comments
  3. Deterministic Tests: Avoid randomness, ensure consistent results
  4. Code Coverage: Aim for good coverage, use reporting tools

Understand and Structure Your Tests Clearly

  • Arrange-Act-Assert Pattern: Structure your tests clearly by separating the setup (arrange), the action (act), and the assertions (assert).
  • Descriptive Test Names: Ensure test descriptions clearly convey what they test and the expected outcomes.
  • Logical Grouping: Organize tests logically around utils, composables or components and their functionalities.

Leverage TypeScript’s Static Typing

  • Use Strong Typing: Define interfaces and types for mocks, data sets, and components under test to avoid type-related runtime errors.
  • Utilize Type Inference: Where possible, rely on TypeScript's type inference to make code cleaner and less verbose.

Mocking and Isolation

  • Use Mocks and Stubs: Isolate units of code by replacing dependencies with mocks or stubs, which can be easily done using Vitest’s utilities like vi.fn() for mocking functions.
  • Type-Safe Mocks: Ensure that mocks adhere to the types of the dependencies they replace, maintaining type safety.
  • Selective Mocking: When mocking external dependencies in unit tests, prioritize dependencies directly impacting the control flow and output of the tested functionality. Avoid excessive mocking to prevent brittle tests and ensure meaningful insights into real-world behavior.

Component Isolation

  • Shallow Mount: Use shallow mounting when testing components to isolate them from their child components, which can be individually tested. This helps in pinpointing where issues occur.
  • Mock Dependencies: Use mocks for external dependencies and services to test components in isolation.

Reactivity and State Management

  • Test Data Reactivity: Ensure that your tests account for Vue’s reactivity system by testing changes in component data and observing the effects on the DOM.

Effective Use of Assertions

  • Choose the Right Assertions: Use appropriate assertions to make your tests readable and meaningful. Vitest provides a rich set of assertions that can handle various scenarios.
  • Test Edge Cases: Include tests for boundary conditions and unusual situations to ensure robustness.

Test Coverage Goals

  • Strive for Meaningful Coverage: Aim for high test coverage but focus on testing the logic thoroughly rather than hitting arbitrary percentage targets.
  • Use Coverage Tools: Use Vitest’s built-in coverage reporters to assess which parts of your codebase need more thorough testing.

Asynchronous Testing

  • Proper Handling of Async Code: Use async / await for testing asynchronous functions to make your tests clean and easy to follow. Ensure that tests complete all operations before assertions.
  • Timeouts and Waits: Be mindful of using timeouts and waitFor functions to handle scenarios involving delayed responses. Use Vitest’s capabilities to control and simulate timers when testing components that rely on time-based functionalities.

Parameterized Tests

  • Use test.each: When you have multiple inputs and outputs for the same piece of logic, use test.each to run the same test logic under different conditions.

Use of Snapshot Testing

  • Minimize usage: As snapshots rely on string comparison, this can lead to a noticeable performance impact during test execution. Instead we should rely on explicit selectors to check changes if possible.
  • Snapshots for UI Changes: Utilize snapshot (inline preferred) testing to capture the rendered output of components, which helps in detecting unintended UI changes.
  • Review Snapshots Carefully: Always review snapshot changes carefully during code reviews to ensure that changes are intentional and correct.

Documentation and Examples:

  • Document Test Cases: Keep documentation for test cases updated to help new developers understand the purpose and coverage of existing tests.
  • Provide Realistic Examples: Use realistic data in tests to mimic real-world scenarios as closely as possible.

Integration with Nuxt

  • Nuxt Runtime Environment: Enable the Nuxt Runtime Environment on a per test base by adding *.nuxt.* to the filename (e.g. my-storefront-function.nuxt.test.ts), this allows to test with the Nuxt application context and reduces the need for extensive dependency and functionality mocking.
  • Nuxt Specific Hooks: Test Nuxt specific hooks and functionality like fetch, asyncData, and plugins ensuring they integrate well within the component lifecycle.
  • Server-Side Rendering: Consider scenarios where components need to be pre-rendered on the server, and ensure your tests reflect both client and server-side behaviors.

Additional Tips

  1. Leverage Nuxt composables for testability
  2. Integrate testing with the CI/CD pipeline

NOTE: Remember that @nuxt/test-utils provides a powerful set of utilities for testing various Nuxt-specific aspects. Use it in conjunction with @vue/test-utils for comprehensive component testing within your Nuxt application.