site
scripts to generate personal blog and git repositories
git clone https://9o.is/git/site.git
commit 0eb5df85e83a93f98d313e386dab1548b08fb94f parent b188f5ff131d258f2fc08c7178ac2beed3773c63 Author: Jul <jul@9o.is> Date: Sat, 9 May 2026 15:59:34 +0800 add jest snapshot testing article Diffstat:
| A | pages/why-snapshot-testing-is-easy.md | | | 131 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 131 insertions(+), 0 deletions(-)
diff --git a/pages/why-snapshot-testing-is-easy.md b/pages/why-snapshot-testing-is-easy.md @@ -0,0 +1,131 @@ +filename: why-snapshot-testing-is-easy-but-not-simple.html +title: Why Snapshot Testing is easy but not simple +description: An analytical critique of Jest snapshot testing, arguing that the practice prioritizes immediate ease over architectural simplicity. By asserting against the entirety of a function's serialized output, snapshots violate the robustness principle and frequently fail due to benign structural modifications. The article explores how this brittleness degrades test suite reliability and advocates for transitioning to targeted, explicit assertions and dedicated visual regression tools. +keywords: testing robustness javascript typescript +created: 2026-05-09 +updated: 2026-05-09 + +<nav aria-labelledby="toc-heading"> + <h2 id="toc-heading">Table of Contents</h2> + <ol> + <li><a href="#robustness-overview">Brief Overview Of The Robustness Principle</a></li> + <li><a href="#robustness-snapshot">Snapshot Tests Violate The Robustness Principle</a></li> + <li><a href="#easy-vs-simple">Easy vs. Simple: The Design Trade-off</a></li> + <li><a href="#ui-limitations">The Limitations of UI Snapshots</a></li> + </ol> +</nav> + +In 2016, Christoph Pojer (at the time a Facebook developer) introduced Jest’s snapshot testing to the world. In a talk with Kent C. Dodds[^1], Pojer posed a question that has influenced modern front-end development: *"What can we do to make it low effort to write unit tests?"* + +The problem Christoph identified was real: developers find writing unit tests tedious. His solution was to automate test writing and make it easier for developers. But in reality, this approach made things harder because the generated tests are more brittle and less maintainable. By choosing "easy" over "simple," snapshot testing didn't solve the problem that developers need to understand when writing unit tests at scale—it made the problem worse. + +## Brief Overview Of The Robustness Principle {#robustness-overview} + +The robustness principle, also commonly known as Postel's Law[^2], states: "be conservative in what you do, be liberal in what you accept from others". Although Postel briefly mentioned the robustness principle for designing internet protocols, programmers have adopted and extended it across many programming contexts—from function design to data processing to API contracts. Consider a Javascript function that calculates the total price of items: + +```javascript +function calculateTotal(items) { + return items.reduce((sum, item) => sum + item.price, 0); +} +``` + +This function is fragile—it crashes if you pass it anything other than an array with a `reduce` method. It will fail with `Set` objects, array-like objects, or iterables. A more robust version accepts what it needs liberally: + +```javascript +function calculateTotal(items) { + let sum = 0; + for (const item of items) { + sum += item?.price ?? 0; + } + return sum; +} +``` + +Now it works with arrays, Sets, or any iterable, and items can be undefined or missing their price property. This example demonstrates how the robust version is "liberal in what it accepts" while remaining "conservative in what it does" (calculating a sum). + +In practice, you'd want to restrict this to only objects with a price property. Being too liberal in what you accept does carry security implications worth considering. RFC 9413[^3] and research in language-theoretic security (LangSec)[^4] by Meredith Patterson have shown that overly permissive input handling can introduce vulnerabilities. For instance, if the function recursively calculated prices in nested objects without depth limits, and the input came from an untrusted user, an attacker could construct deeply nested structures to trigger a denial-of-service attack. In this context, it's typically safe for input objects to contain extra properties that the function simply ignores. + +To restrict our function to only accept objects with a price property, we could use TypeScript to check for us. + +```typescript +function calculateTotal(items: Iterable<{ price: number }>) { + let sum = 0; + for (const item of items) { + sum += item.price; + } + return sum; +} +``` + +Robustness in type theory extends well beyond simple input validation to encompass more sophisticated concepts like contravariance, where functions are deliberately designed to accept broader, more general types than strictly necessary for their immediate implementation (being liberal in what they accept as input). This flexibility allows functions to work with a wider range of arguments while maintaining type safety. The "L" in SOLID—the Liskov Substitution Principle—formalizes this fundamental idea of robustness at the type level through the concept of behavioral subtyping. The principle states that objects of a superclass should be replaceable with objects of a subclass without breaking the application, ensuring that derived types extend base types without altering their desirable properties and behaviors. + +## Snapshot Tests Violate The Robustness Principle {#robustness-snapshot} + +In software testing, it helps to think from a meta-programming perspective: the code is your input (or data), and the test results are your output. The goal is to write conservative tests that liberally accept different implementations, so software modifications don't unintentionally break existing tests. In other words, your unit tests should be robust to change. A test should only fail when the specific behavior it checks is actually broken. + +Snapshot testing achieves the exact opposite. Because a snapshot asserts the *entire* output of a function or component, it's inherently brittle. A single, irrelevant change to any property breaks every test that depends on that function. + +Consider a function that returns user data: + +```javascript +function getUser(id) { + return { id, name: "Alice", email: "alice@example.com" }; +} +``` + +With snapshot testing, your test captures everything: + +```javascript +expect(getUser(1)).toMatchSnapshot(); +// Snapshot: { id: 1, name: "Alice", email: "alice@example.com" } +``` + +Now add a `createdAt` timestamp to the user object: + +```javascript +function getUser(id) { + return { id, name: "Alice", email: "alice@example.com", createdAt: "2024-01-01" }; +} +``` + +**Every snapshot test breaks**, even though the core behavior (returning user data) hasn't changed. If you have 10 snapshot tests using this function, you now have 10 failing tests instead of zero. + +The robust alternative uses targeted assertions: + +```javascript +expect(getUser(1)).toHaveProperty("name", "Alice"); +expect(getUser(1)).toHaveProperty("email", "alice@example.com"); +``` + +These assertions remain true when `createdAt` is added—they only fail when the specific properties they care about actually break. I've led teams and scaled projects with hundreds of unit tests, and I've learned this: if developers don't follow basic principles like robustness, adoption of unit testing will fall apart. Does that mean instead of writing 10 unit tests, you'll need to write 20 and type more? Yes, but that's the price of building something simple rather than easy. + +## Easy vs. Simple: The Design Trade-off {#easy-vs-simple} + +In his talk, Pojer identifies the manual creation of assertions as a point of friction: "You try to write a test and you have this object and you try to match [it with] toEqual... how can we make this easier?" + +This approach highlights a common tension in software design: prioritizing immediate ease over architectural simplicity. While generating and updating a snapshot requires very low immediate effort, it tightly couples the test to the entirety of the function's serialized output. In contrast, a simple test relies on targeted assertions that verify specific invariants, decoupling the test suite from irrelevant structural modifications. + +Because snapshots assert against the complete output, they frequently fail due to benign changes. When a test suite generates a high volume of false positives, developers can experience alert fatigue. This often leads to a routine of automatically updating the snapshots without strictly reviewing the code modifications, ultimately degrading the reliability of the test suite as a functional verification tool. + +## The Limitations of UI Snapshots {#ui-limitations} + +A common rationale for maintaining snapshot tests is their application to UI components[^5][^6]. However, a user interface is a visual output derived from the complex interplay of HTML, CSS, and browser rendering pipelines. A Jest snapshot strictly captures the serialized HTML structure. + +Because the testing tool does not evaluate the rendered output, evaluating HTML markup is an unreliable method for verifying visual correctness. An updated CSS utility class will fail a snapshot test, but inspecting the resulting HTML diff provides little assurance to developers that the visual rendering remains intact. + +Test objectives should dictate the tooling. If the goal is to verify component semantics or accessibility attributes, explicit assertions remain the most robust approach (e.g., `expect(button).toHaveAttribute('aria-expanded', 'true')`). Conversely, if the objective is to guarantee visual consistency, the correct technical solution is Visual Regression Testing, which evaluates actual pixel renderings. + +Ultimately, snapshot testing occupies an ambiguous technical space: it lacks the targeted precision of a unit test and the visual accuracy of an end-to-end rendering test. By archiving the current state of an output rather than defining its required functional constraints, snapshots record what the data is rather than what it must be. Transitioning away from snapshots requires writing explicit assertions, which demands more upfront effort but yields a more resilient and meaningful test suite. + + +[^1]: Christoph Pojer. "Jest Snapshot Testing." Talk with Kent C. Dodds on the Testing JavaScript podcast. <https://www.youtube.com/watch?v=i31VtyJSM-I&t=17m58s> + +[^2]: Jon Postel. "Transmission Control Protocol." RFC 793, September 1981. <https://tools.ietf.org/html/rfc793> + +[^3]: IETF. "Maintaining Robust Protocols." RFC 9413, June 2023. <https://tools.ietf.org/html/rfc9413> + +[^4]: Meredith Patterson and Sergey Bratus. "Language-Theoretic Security." Foundational research on formal language theory and input validation as a security mechanism. <https://langsec.org> + +[^5]: "Snapshot Testing." Jest 24.x Archived Documentation. <https://archive.jestjs.io/docs/en/24.x/snapshot-testing> + +[^6]: Ben McCormick. "Testing with Jest Snapshots: First Impressions." September 2016. <https://benmccormick.org/2016/09/19/074100.html>