when green tests stop proving anything
Snapshot testing promises quick protection against unintended changes to Vue components, but in practice it often tips into meaningless green checkmarks. Unstable snapshots, thoughtless updates, and missing reviews turn a sensible tool into a facade that lets real regressions through unnoticed.
Table of contents
- 1. What snapshot testing promises and where it tips over
- 2. Understanding serialization of Vue components
- 3. Unstable snapshots: IDs, timestamps, random data
- 4. Snapshot drift: green without real protection
- 5. Using inline vs. file snapshots deliberately
- 6. Snapshotting output vs. behavior
- 7. Review workflow for snapshot updates in a team
- 8. When snapshot testing is the wrong choice
- 9. Snapshot vs. explicit assertions compared
- 10. Summary
- 11. FAQ
1. What snapshot testing promises and where it tips over
Snapshot testing promises, at first glance, an appealingly simple solution: instead of writing individual assertions about the rendered output of a Vue component, the entire output is stored as a reference on the first test run and automatically compared on every subsequent run. If the current output deviates from the stored snapshot, the test fails. That sounds like free, comprehensive protection with no manual effort.
In practice, however, this promise regularly falls apart, because snapshot testing makes an implicit assumption that is rarely upheld: that every deviation from the stored snapshot actually gets a developer review before being accepted. Once teams habitually run vitest --update without reading the actual diff, snapshot testing degrades into a ritual that produces green checkmarks but no longer prevents real regressions.
This article examines the concrete pitfalls that turn snapshot testing in Vue projects from a valuable safety net into a deceptive facade, and shows where explicit assertions are the more robust alternative.
2. Understanding serialization of Vue components
Before snapshot testing can be used sensibly, it must be clear what is actually being serialized. Vitest with @vue/test-utils by default serializes a component's rendered HTML via wrapper.html(), not the internal Vue instance or the reactive state. That means a snapshot test only checks the DOM output at a given point in time, not the underlying logic that produced that output.
This limitation is important to understand: two completely different implementations of a component that happen to produce the same HTML output result in identical snapshots. A snapshot test therefore never confirms that a component works correctly, only that the output has not changed since the last run. For pure regression testing on stable, deterministic markup that is sufficient, for behavior verification it is not.
// ProductBadge.test.js — basic snapshot test, serializing rendered HTML
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import ProductBadge from "./ProductBadge.vue";
describe("ProductBadge", () => {
it("matches the snapshot for a discounted product", () => {
const wrapper = mount(ProductBadge, {
props: { discountPercent: 20, label: "Sale" },
});
// Only the rendered HTML is captured — not the component's internal logic
expect(wrapper.html()).toMatchSnapshot();
});
});
3. Unstable snapshots: IDs, timestamps, random data
The most common technical mistake in snapshot testing is snapshotting components whose output is not deterministic. Vue components that use Date.now(), Math.random(), or auto generated IDs for aria-describedby or form elements produce a slightly different output on every test run. The snapshot comparison then fails on every run, even when the actual component logic has not changed at all.
The fix is to mask or inject all non-deterministic values before the snapshot comparison. For timestamps, vi.setSystemTime() is a good fit, for random numbers a mock of Math.random, and for auto generated IDs, a deterministic ID factory that is replaced with a fixed sequence in the test. Without these measures, snapshot testing quickly becomes a source of constant, meaningless failures that tempt teams into reflexively updating snapshots without reviewing the diff.
// OrderReceipt.test.js — stabilizing non-deterministic values before snapshotting
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mount } from "@vue/test-utils";
import OrderReceipt from "./OrderReceipt.vue";
describe("OrderReceipt", () => {
beforeEach(() => {
// Freeze time so the rendered timestamp is deterministic
vi.setSystemTime(new Date("2026-07-30T10:00:00Z"));
});
afterEach(() => vi.useRealTimers());
it("matches the snapshot with a frozen system time", () => {
const wrapper = mount(OrderReceipt, {
props: { orderId: 1042, total: 89.9 },
});
// No more flaky diffs from Date.now() timestamps
expect(wrapper.html()).toMatchSnapshot();
});
it("normalizes generated IDs before matching the snapshot", () => {
const wrapper = mount(OrderReceipt, { props: { orderId: 1042, total: 89.9 } });
const normalized = wrapper.html().replace(/id="field-[a-z0-9]+"/g, 'id="field-normalized"');
expect(normalized).toMatchSnapshot();
});
});
4. Snapshot drift: green without real protection
Snapshot drift describes the gradual process where snapshots drift further and further from their originally, deliberately checked state through many small, unreflected updates, without anyone ever actually evaluating the cumulative change. A single vitest --update after a small CSS change seems harmless. But after twenty such updates over several months, the snapshot may have completely departed from the original design intent, without a single one of those changes ever having been deliberately judged correct.
The real problem is not the snapshot technique itself, but the workflow around it. When vitest --update runs in the CI pipeline, or as the default reaction to every failing test, the snapshot loses its function as regression protection entirely. The test goes green, but the fundamental question of whether the change was actually intended is never asked. That is exactly what distinguishes snapshot drift from a legitimate, deliberate snapshot update after a review.
5. Using inline vs. file snapshots deliberately
Vitest supports both classic file snapshots via toMatchSnapshot(), stored in a separate __snapshots__ directory, and inline snapshots via toMatchInlineSnapshot(), where the expected value is embedded directly in the test code as a string. Inline snapshots automatically force a review through their visibility in the pull request diff, since every change to the snapshot is directly visible in the code, instead of being hidden in a separate file that many reviewers skip during code review.
For small, targeted snapshots, such as the output of a single formatting function, an inline snapshot is almost always the better choice. For full component renderings with extensive HTML, an inline snapshot quickly becomes unwieldy, and a file snapshot remains more practical, as long as the review process deliberately includes the snapshot diffs in __snapshots__ and not just the source code.
// priceFormatter.test.js — inline snapshot forces the diff into the code review
import { describe, it, expect } from "vitest";
import { formatPrice } from "@/utils/priceFormatter";
describe("formatPrice", () => {
it("formats a price with two decimal places and currency symbol", () => {
expect(formatPrice(79.9, "EUR")).toMatchInlineSnapshot(`"79.90 EUR"`);
});
it("rounds correctly for prices with more decimal places", () => {
expect(formatPrice(19.995, "EUR")).toMatchInlineSnapshot(`"20.00 EUR"`);
});
});
6. Snapshotting output vs. behavior
A central mistake in thinking about snapshot testing is treating it as a substitute for behavior tests. A snapshot only confirms that the output has not changed, it says nothing about whether that output is actually correct. A button that is incorrectly rendered as disabled gets the same green snapshot test as a button correctly rendered as enabled, as long as the wrong state was captured from the very first snapshot.
Explicit assertions such as expect(button.attributes('disabled')).toBeUndefined(), on the other hand, express a deliberate expectation that is checked independently of the current snapshot state. The most robust practice combines both approaches: snapshot testing for the rough structure and CSS classes of a component, explicit assertions for the business relevant states such as disablement, error display, or computed values.
7. Review workflow for snapshot updates in a team
For snapshot testing to retain its value, it needs a clear team workflow for updates. The most important rule: vitest --update must never run automatically in the CI pipeline, only locally by a developer who actively reads the generated diff and deliberately decides whether the change is correct. The updated snapshot then belongs in version control as its own, clearly named commit, or as part of the pull request that contains the actual change.
In the code review itself, reviewers should take snapshot diffs just as seriously as source code changes, not skim past them as a mere formality. A pull request that updates twenty snapshot files in a single commit with no description is a warning sign that the snapshots may have been reflexively updated without evaluating the individual changes.
8. When snapshot testing is the wrong choice
Snapshot testing is the wrong choice for components with a high rate of change, for example during active UI development, when markup and styling still change several times a day. During this phase, every commit produces a failing snapshot test that must be updated immediately, which completely undermines the actual purpose of the test, detecting unintended changes.
Snapshot testing is equally unsuitable for components with business critical logic, such as price calculations, discount logic, or validation rules. Here you need explicit assertions that verify business correctness independent of visual output. Snapshot testing works best for stable, rarely changed presentation components whose output can be clearly derived from manageable props, such as icons, badges, or simple layout wrappers.
9. Snapshot vs. explicit assertions compared
The decision between snapshot testing and explicit assertions should be made deliberately, per component, depending on rate of change and business criticality.
| Criterion | Snapshot testing | Explicit assertions |
|---|---|---|
| Effort to create | Very low, a single call | Higher, every expectation explicitly written |
| Clarity on failure | Low, often a huge diff | High, clear error message |
| Risk of thoughtless updates | High (vitest --update) | Low, change must live in the code |
| Suitable for business logic | No | Yes |
| Suitable for stable presentation | Yes | Possible, but more boilerplate |
In most Vue projects, a mix works best: snapshot testing for stable presentation components with low change frequency, explicit assertions for anything with business meaning. Whoever draws this line deliberately avoids both the boilerplate overhead of unnecessary explicit tests and the deceptive safety of meaningless snapshot greenness.
Mironsoft
Test architecture audits and reliable test suites for Vue applications
Snapshot tests nobody actually reads anymore?
We audit existing test suites for snapshot drift, replace meaningless snapshot tests with targeted assertions, and set up a review workflow that makes snapshot updates meaningful again.
Test audit
Checking existing snapshot tests for drift and meaningfulness
Refactoring
Converting business critical tests from snapshots to explicit assertions
Team workflow
Establishing a review process for snapshot updates, preventing CI auto-updates
10. Summary
Snapshot testing pitfalls rarely arise from the technique itself, but almost always from the workflow around it. Unstable snapshots caused by timestamps, random data, or generated IDs can be avoided with frozen system time and deterministic mocks. Snapshot drift arises when vitest --update runs reflexively instead of after a deliberate review. Inline snapshots automatically force more attention through their visibility in the diff than hidden file snapshots.
The most important principle is to never treat snapshot testing as a substitute for behavior tests. Business critical logic always belongs in explicit assertions that are checked independent of the current snapshot state. Snapshot testing delivers its value best for stable, rarely changed presentation components, combined with a team workflow that treats every snapshot update as a deliberate decision, not an automatic formality.
Snapshot Testing Pitfalls in Vue — the essentials at a glance
Unstable snapshots
Mask timestamps, random data, and generated IDs before comparison, or control them with fake timers.
Snapshot drift
Never run vitest --update in CI, always locally with a deliberate review of the diff.
Output vs. behavior
Snapshots only check output equality, never business correctness. Always use explicit assertions for logic.
Correct usage
Only for stable, rarely changed presentation components, not for active UI development or business logic.