docs

🔗 Unit Testing

Overview

As technical complexity rises, so does the potential for side effects and bugs. It is therefore crucial to verify and test critical code. The SCAYLE Storefront Application comes with a range of preconfigured unit test suite to support this.

The unit test coverage of the Storefront Application is continuously extended with every release. The current focus lies on functional testing for composables and utilities, as well as getting extended to include Vue component testing (integration testing).

Getting Started

By leveraging the testing framework Vitest with @nuxt/test-utils integration, we can test individual functions, composables, and Vue components in isolation. This approach allows us to identify bugs early, validate business logic, and ensure components behave correctly under various conditions. Ultimately, a robust unit and component testing suite contributes significantly to the overall code quality, maintainability, and reliability of our storefront applications.

Prerequisites

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

Testing Environment Setup

To extend testing capabilities, it also utilizes @vue/test-utils, @nuxt/test-utils, and happy-dom. These dependencies collectively cover a wide range of potential unit test cases within the context of a Vue and Nuxt application.

@vue/test-utils

@vue/test-utils is a library for testing Vue components. It provides utilities to:

  • Mount components: Mount individual components in isolation or as part of a larger tree.
  • Interact with components: Trigger events, update props, and manipulate component state.
  • Assert on output: Make assertions about rendered HTML, data, and computed properties.
  • Control rendering: Choose from shallowMount, mount, or render based on testing needs.
  • Work with Slots: Test how components manage content passed through slots.
  • Simplify Async Operations: Conveniently test asynchronous actions (e.g., API calls, transitions) using async/await and nextTick.
  • Access Component Instance: Directly access the underlying Vue instance for lifecycle hooks, methods, or internal state.
  • Mock Dependencies: Easily replace external dependencies with mocks to isolate component logic.
  • Improve Test Readability: Write cleaner tests with helper methods for finding elements and triggering events.

@nuxt/test-utils

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

  • Mount Nuxt Components: Mount components while ensuring Nuxt plugins, middleware, and other configurations are correctly applied, mimicking a real Nuxt environment.
  • Test Server-Side Rendering (SSR): Test the server-side rendering behavior of your Nuxt components and pages.
  • Interact with Nuxt Context: Access and interact with the Nuxt context ($nuxt) within your tests for navigation, route data, or store interactions.
  • Mock Nuxt Modules: Easily mock the behavior of Nuxt modules to isolate functionalities or test configurations.
  • Handle Asynchronous Data Fetching: Test components and pages that rely on asynchronous data fetching methods (asyncData, fetch, nuxtServerInit).

Running Tests

Tests can be executed from the root of the Storefront Application. By default, all test files matching the naming conventions are executed when the command is triggered.

Standard Test Execution

Test Execution Options

Writing and Customizing Storefront Unit Tests

Test File Naming & Location Convention

The Storefront Application follows specific naming conventions for test files:

  • Unit Tests: *.test.ts for utility functions and composables
  • Nuxt Environment Tests: *.nuxt.test.ts for components requiring Nuxt context
  • Component Tests: *.test.ts for Vue components without Nuxt dependencies

Test Data Factories with Fishery

The Storefront Application uses Fishery for creating realistic test data. Fishery provides a powerful factory pattern that allows you to build complex, related objects with proper relationships and realistic data.

Key Benefits of Fishery:

  • Generate data that closely resembles production data.
  • Create related objects with proper foreign key relationships.
  • Define reusable traits for common variations.
  • Generate unique values for fields like IDs, emails, etc.
  • Full TypeScript support with proper type inference.

Example Factory Usage

Custom Test Data Factories

Testing Composables

Composables are an integral part of the Storefront Application and should be thoroughly tested. When testing composables that use Nuxt-specific functionality, use the .nuxt.test.ts file extension to enable the Nuxt runtime environment.

Example Composable Test

Testing Vue Components

The Storefront Application to include a growing and comprehensive testing coverage for Vue components.

Example Component Test with Nuxt Dependencies

Example Composable Test with Component Wrapper:

Adding New Test Cases

  1. Add test cases to existing describe blocks:
  1. Add new describe blocks for different scenarios:

Best Practices for Storefront Unit Tests

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.

Important Notes

CAUTION: Unit and component tests should be fast and reliable. Avoid overly complex test cases.

Always ensure you have:

  • Proper test isolation and cleanup
  • Appropriate mocking of external dependencies
  • Clear test naming and documentation
  • Realistic test data and scenarios

Best Practices:

  • Write tests that are easy to understand and maintain
  • Use descriptive test names that explain the expected behavior
  • Mock external dependencies to ensure test reliability
  • Keep tests focused on single behaviors or functionalities
  • Use test data factories for consistent and maintainable test data
  • Leverage Fishery for creating realistic test data with proper relationships
  • Test both happy path and edge cases
  • Ensure tests are deterministic and don't rely on external state

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.