-
Notifications
You must be signed in to change notification settings - Fork 26
Description
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:
- https://github.com/Particular/ServicePulse/blob/master/src/Frontend/src/composables/serviceLicense.ts
- https://github.com/Particular/ServicePulse/blob/master/src/Frontend/src/composables/serviceServiceControl.ts
- https://github.com/Particular/ServicePulse/blob/master/src/Frontend/src/composables/serviceServiceControlUrls.ts
- Everything in https://github.com/Particular/ServicePulse/blob/master/src/Frontend/src/composables/service**.ts
- Create a PR template with a checklist PR reviewers can use to verify good practices are being followed when it comes to global state, for example, a checklist that includes an item like:
- 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
- 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
- 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.
- Refactor Composables to Avoid Module-Level State; instead, initialize state within functions or use a factory pattern. However this creates state everytime the
useComposablefunction 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.