Skip to content

Refactor remaining composables used for global state management to use Pinia stores to resolve potential flaky acceptance tests caused by module-level state #1905

@TravisNickels

Description

@TravisNickels

Describe the suggested improvement

Is your improvement related to a problem? Please describe.

Our current Vue.js application has some remaining composables that maintain state at the module level. While this approach simplifies state sharing across components, it introduces significant issues in our acceptance tests conducted with Vitest and JSDOM. Specifically, the shared module-level state could lead to flaky tests due to state persistence between test runs.

Note

The application already uses Pinia stores. See https://github.com/Particular/ServicePulse/tree/master/src/Frontend/src/stores. This issue is about completing the migration to Pinia but emphasizes the importance of this work to help with automated testing efforts.

Testing challenges when using module-level state

  • Shared State Across Tests:

    • JavaScript modules are singletons, meaning the module-level state in our composables persists across different test cases.
    • Tests inadvertently affect each other's outcomes because they operate on the same shared state, leading to unpredictable results.
  • Flaky Tests:

    • Tests may pass or fail inconsistently depending on the order of execution and the residual state left by previous tests.
    • This flakiness undermines the reliability of our test suite and complicates continuous integration efforts.
  • Testing Challenges with Vitest and JSDOM:

    • Vitest and JSDOM do not automatically isolate module-level state between tests.
    • Workarounds to reset the state between tests are non-trivial and can add unnecessary complexity to the testing setup.

Impact

  • Reduced Confidence in Test Results:

    • Flaky tests make it difficult to discern genuine issues from false negatives or positives.
    • Developers may spend excessive time debugging tests that fail due to shared state rather than actual code defects.
  • Slower Development Cycle:

    • Time spent troubleshooting flaky tests slows down the development process.
    • Potential for real bugs to be overlooked if tests are assumed to be unreliable.

Describe the suggested solution

Refactor our composables to use Pinia stores instead of maintaining state at the module level. Pinia is the recommended state management library for Vue.js that offers reactive and typesafe stores.

Many, if not all, of the "composables" in the Frontend\src\composables folder can be either moved to Pinia stores or refactored into general functions, with their names adjusted to not start with "use".

With testability in mind, composables should primarily only be used for organizing shared functionality and logic, rather than for global state management.

Note

At the moment of creating this issue, there are no flaky tests, and tests can be run in groups, individually, and in whatever order without being flaky. However, without this refactoring any developer that wishes to write new acceptance test will need to be aware of the limitation and how to navigate around it, e.g. [1],[2]

PoA

Refactor to Pinia stores the following files:

  • Ensure no module-level state is being introduced. If so, request the PR author to move the state to the corresponding Pinia store.

Describe alternatives you've considered

  1. Do not use JSDOM for acceptance testing and do not directly instantiate the app, instead use a headless browser. However, the idea behind using a virtual DOM (JSDOM) instead of a real browser is to have acceptance tests that run way faster than tests that require spinning a whole headless browser
  2. Manually reset the state to the expected values before running the test case. This ties the test to implementation details which is something that is preferred to avoid at the acceptance test level.
  3. Refactor Composables to Avoid Module-Level State; instead, initialize state within functions or use a factory pattern. However this creates state everytime the useComposable function is invoked, defeating the purpose of having state that benefits from being global.

Module-Level State in Composable:

// myComposable.js
import { ref } from 'vue';

// Module-level state (shared across imports)
const globalState = ref(0);

export function useMyComposable() {
 return {
   globalState,
 };
}

State instance per call
(not useful for state that is meant to be shared across components)

// myComposable.js
import { ref } from 'vue';

export function useMyComposable() {
  const state = ref(0);
  return {
    state,
  };
}

Advantages of Using Pinia

Although the application is already benefiting from using Pinia, here are some of the advantages of using it when it comes to testing

  • Isolated State Per Test Run:

    • Each store instance is scoped to the app or test instance, preventing unintended state sharing.
    • Eliminates side effects caused by shared state in module-level variables.
  • Improved Developer Experience:

    • Pinia integrates seamlessly with Vue DevTools for easier debugging.
    • Provides a clean and intuitive API for state management
  • Improved Test Reliability:

    • Tests become deterministic and reliable, enhancing confidence in test outcomes.
    • Simplifies the testing process by removing the need for complex state reset mechanisms.
  • Enhanced Codebase Maintainability:

    • Adopting Pinia aligns with Vue's recommended state management practices.
    • Provides a more scalable and maintainable approach as the application grows.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions