From 4958713ec4ea78e82f1b908e4e250c48d15eb1a5 Mon Sep 17 00:00:00 2001 From: IT-WIBRC Date: Wed, 27 Aug 2025 12:51:35 +0100 Subject: [PATCH] docs: the Hidden Power of Your Test Setup and Mocks --- README.md | 7 + TODO.md | 2 +- ...dden-power-of-your-test-setup-and-mocks.md | 148 ++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 remarks/testing/the-hidden-power-of-your-test-setup-and-mocks.md diff --git a/README.md b/README.md index 992f0a4..519305f 100644 --- a/README.md +++ b/README.md @@ -179,11 +179,17 @@ Insights into testing methodologies and specific tool configurations. - Alias and auto-import issues in test files. - IDE behavior differences. - [**Testing Reactive Composables in Nuxt & Vitest: Overcoming Mocking Challenges**](./remarks/testing/vitest-nuxt-reactive-mocks.md) + - Vitest's strict matchers (`toBe`, `toEqual`, `toStrictEqual`). - The "different instances" problem with reactive mocks in Nuxt/Vitest. - Why global mocks in `vitest.setup.ts` can fail for reactive state. - A robust pattern for centralizing singleton mocks and applying them locally. +- [**The Hidden Power of Your Test Setup and Mocks**](./remarks/testing/the-hidden-power-of-your-test-setup-and-mocks.md) + - Key points about effective test setup and the strategic use of mocks. + - How to avoid common pitfalls when mocking dependencies. + - Best practices for maintaining clean and reliable tests. + --- ## Presentation-Style Remarks @@ -274,6 +280,7 @@ This section outlines the current directory and file structure for the `coding-r │ │ ├── mocking-useRuntimeConfig-vitest.md │ │ ├── nuxt-virtual-module-resolution.md │ │ ├── test-file-exclusion-and-aliases.md +│ │ ├── the-hidden-power-of-your-test-setup-and-mocks.md │ │ └── vitest-nuxt-reactive-mocks.md │ ├── typescript-javascript/ # TypeScript & JavaScript specific remarks │ │ ├── async-await-explained.md diff --git a/TODO.md b/TODO.md index 7eca3ca..db16d2f 100644 --- a/TODO.md +++ b/TODO.md @@ -28,7 +28,7 @@ - [ ] `eslint-plugin-jsx-a11y` (Test using screen readers like NVDA or VoiceOver) — SEO - [ ] Determine circular dependencies in JavaScript projects: (This ESLint plugin includes the import/no-cycle rule, vite-plugin-circular-dependency (for Vite), Tools like NX and Rush, often used in monorepo setups, Extensions like "Circular Dependencies Finder") -- [ ] Advantages of the `vitest.setup.t`s and `__mocks__` +- [x] Advantages of the `vitest.setup.t`s and `__mocks__` - [ ] `input` autocomplete property - [ ] Don't use the `.d.ts` file for handwriting types inside your file for projects that are not a library as it can be harmful, use a normal `.ts` file instead (recommendation from the TS team). To enforce it, use `skipLibCheck` set to `true`. - [ ] Always search for repeating code (title, description, button, etc.) and create a component to reuse. diff --git a/remarks/testing/the-hidden-power-of-your-test-setup-and-mocks.md b/remarks/testing/the-hidden-power-of-your-test-setup-and-mocks.md new file mode 100644 index 0000000..932a840 --- /dev/null +++ b/remarks/testing/the-hidden-power-of-your-test-setup-and-mocks.md @@ -0,0 +1,148 @@ +# The Hidden Power of Your Test Setup and Mocks + +Have you ever looked at a test file and seen the same setup code repeated over and over? Maybe you're mocking the same API service, or setting up a global `localStorage` mock in dozens of different files. This isn't just a nuisance; it's a huge problem. This leads to **duplication** and **tight coupling**, making your tests difficult to maintain. + +We need a way to **decouple our tests from our dependencies**. + +--- + +## Centralizing with `vitest.setup.ts` + +The `vitest.setup.ts` or `jest.setup.ts` file is a central place for code that should run **before every test file**. Its main advantage is that it allows you to configure global test settings and, most importantly, **set up global mocks**. + +Imagine you have an API service for user authentication that's used across your application. Instead of mocking it in every test file, you can do it once in your setup file. + +Here's a simple `vitest.setup.ts` example: + +```typescript +// vitest.setup.ts + +import { vi } from "vitest"; +import * as authService from "@/services/authService"; + +// We export the mocked functions so we can access and override them +export const isUserLoggedIn = vi.spyOn(authService, "isLoggedIn").mockReturnValue(true); +export const getCurrentLoggedInUser = vi + .spyOn(authService, "getCurrentUser") + .mockReturnValue({ id: 1, name: "Test User" }); +``` + +Now, every single test you run will automatically use this mock for `authService`. You no longer need to write `vi.mock()` at the top of every file. This saves time, reduces duplication, and makes your tests much cleaner. + +The primary benefit is **maintainability**. If you need to update the `authService` mock, you only have to change it in one place. + +--- + +## Flexibility: Overriding Mocks in Specific Tests + +This is where the real power comes in. While you have a global default mock, sometimes you need a specific test to behave differently. By exporting the mocked functions from your setup file, you can easily override their return values for a particular test case. + +Let's say you have a component that should show a different message if the user is not logged in. You can write a test for this specific scenario like this: + +```typescript +// SomeComponent.test.ts + +import { mount } from "@vue/test-utils"; +import SomeComponent from "@/components/SomeComponent.vue"; +import { isUserLoggedIn } from "../../vitest.setup.ts"; // Import the mocked function + +describe("SomeComponent", () => { + it("should show a login message if the user is not logged in", async () => { + // Override the globally mocked function for this specific test + isUserLoggedIn.mockReturnValueOnce(false); + + const wrapper = mount(SomeComponent); + expect(wrapper.text()).toContain("Please log in to continue."); + }); + + // Other tests will still use the default mock of `isUserLoggedIn` returning true. +}); +``` + +This approach combines the best of both worlds: a **global default** mock for most tests, but the flexibility to **customize** the mock's behavior for tests that require a different scenario. This is a crucial aspect of writing robust and comprehensive tests. + +--- + +## Advanced Mocking: Partial Mocks and Hoisting + +Sometimes you don't want to mock an entire module; you just want to replace one or two functions while keeping the rest of the original implementation. This is where more advanced mocking techniques come into play. + +### Mocking with `vi.hoisted` + +Normally, you can't access variables from your test file inside a `vi.mock` factory. **`vi.hoisted`** solves this by "hoisting" the mock definition to the top of the file, allowing you to create dynamic mocks. This is useful when your mock's behavior depends on a variable you define in your test. + +You can also use `vi.hoisted` to create a reusable mock function that can be used inside multiple `vi.mock` calls, making your mock logic cleaner and more consistent. + +```typescript +// SomeComponent.test.ts + +import { vi } from "vitest"; + +const mockedGetPosts = vi.hoisted(() => { + return vi.fn(() => + Promise.resolve([ + { id: 1, title: "Mock Post 1" }, + { id: 2, title: "Mock Post 2" }, + ]), + ); +}); + +vi.mock("@/api", () => ({ + getPosts: mockedGetPosts, // Reusing the hoisted mock function +})); + +describe("SomeComponent", () => { + it("should fetch and display posts on mount", async () => { + // The `getPosts` function is already the mocked version + const wrapper = shallowMount(SomeComponent); + await wrapper.vm.$nextTick(); + expect(mockedGetPosts).toHaveBeenCalled(); + }); +}); +``` + +This is a more complex but very powerful way to create flexible mocks. + +### Partial Mocks with `vi.importActual` + +For more fine-grained control, you can use **`vi.importActual`** (or **`vi.importOriginal`** for CommonJS modules) inside a mock factory. This function gives you access to the real, un-mocked module, allowing you to only mock the parts you need. + +Let's say you only want to mock the `isLoggedIn` function but keep the original `getCurrentUser` function. + +```typescript +// authService.test.ts + +import { vi } from "vitest"; + +vi.mock("@/services/authService", async (importActual) => { + const actualAuth = await importActual(); + return { + ...actualAuth, // Use all original exports + isLoggedIn: vi.fn(() => true), // But override this one + }; +}); +``` + +This ensures that your test only overrides the specific behavior it needs to, further reducing the coupling between your test and the implementation details of the module. + +--- + +## The Magic of the `__mocks__` Directory + +While `vitest.setup.ts` is great for global mocks, the `__mocks__` directory is the perfect solution for **automatic mocking of modules**. + +When you place a file with the same name as a module inside a `__mocks__` directory, Vitest (and Jest) will automatically use that file as the mock whenever the original module is imported. You can even use `vi.importActual` within this mock file to create partial mocks automatically. + +The `__mocks__` directory provides **loose coupling** between your tests and your implementation. Your test file has no idea that `fetchPosts` is being mocked; it simply imports it and uses it. This makes your tests more robust and less likely to break when you refactor your original code. + +--- + +## The Ultimate Payoff: Decoupled and Resilient Tests + +The real power of these tools is that they allow you to create tests that only care about the **behavior** of your code, not its internal dependencies. + +- **`vitest.setup.ts`** is for **global, application-wide mocks** that apply to all your tests. You can also export these mocks for overriding. +- **`__mocks__`** is for **automatic, module-level mocks** that apply whenever a specific module is imported. +- **`vi.hoisted`** and **`vi.importActual`** provide the necessary flexibility for creating advanced, partial mocks when simple mocking isn't enough. + +By using these tools, you're not just making your tests shorter. You're building a testing strategy that's resilient to change. You can refactor your services, change your API endpoints, or switch data libraries, and your tests will remain intact, as long as the mocked behavior is consistent.