Yashveer Singh
Connect
<- All posts
Tech Debt and Refactoring7 min read

Snapshots, Property Tests, and the Modern Test Toolbox

Unit tests and integration tests are not the whole story. Here is what else belongs in a serious test suite.

Written by Yashveer Singh, founder of Yashveer Labs.

# Snapshots, Property Tests, and the Modern Test Toolbox

Most engineering teams cover unit tests and maybe integration tests, then stop. Snapshot tests, property-based tests, mutation tests, and contract tests each catch a different class of bug that traditional testing misses. Understanding which tool catches which class of problem is the difference between a test suite that builds confidence and one that passes CI while hiding production failures.

What you need to know

  • Snapshot tests catch unintended UI and output changes; they are fast to create but expensive to maintain if misused
  • Property-based tests generate hundreds of random inputs and find edge cases your hand-written tests never would have considered
  • Mutation testing checks whether your tests are actually doing their job by introducing deliberate bugs and verifying the suite catches them
  • Contract tests ensure that the API contract between services matches on both sides, catching integration failures before deployment
  • Most codebases benefit from two or three of these tools strategically applied, not all of them everywhere

The core argument

The failure mode of most test suites is not that they are too small; it is that they test what the developer expected to happen rather than what could happen. A hand-written test for a function that formats a date will test the obvious cases: a normal date, a null input, a leap year. It probably will not test the case where the timezone offset causes the date to flip to the previous day, or where a non-standard locale changes the decimal separator in unexpected ways. Property-based testing generates hundreds of random inputs automatically and finds the edge cases you forgot to imagine.

I introduced property-based testing to the Nexli codebase for the fee calculation logic. Fee calculations involve currency rounding, percentage-based additions, conditional discounts, and edge cases around zero-value line items. Hand-written tests covered the normal cases well. Property-based tests (using fast-check for TypeScript) found three rounding errors within the first run that were present in production but had never been triggered by the normal test inputs. The fix was two lines. The alternative discovery path was a confused parent with an incorrect invoice.

Snapshot testing gets misused more than any other tool. The pattern is: write a component, run the snapshot test, commit the snapshot, and from then on any change to the component output requires updating the snapshot. The problem is that snapshot updates become a mechanical step in every PR, and the "does the snapshot look right" review stops happening because reviewers stop reading diffs they can regenerate. Used well, snapshots are for stable, thoroughly designed outputs where unexpected changes are genuinely a signal of a problem. Used badly, they are a ceremony that adds noise to every PR without catching anything meaningful.

Common mistakes

  1. Treating snapshot failures as "update and move on." A failing snapshot test is either a legitimate problem (something changed that should not have) or a test that should not exist (testing implementation details rather than behavior). The answer is not automatically to update the snapshot.
  1. Using property-based tests without understanding what properties you are actually testing. Property-based tests require you to define the invariant you are testing: "this function always returns a positive number" or "this function produces the same result regardless of input order." Without a clear invariant, the test does not communicate intent.
  1. Running mutation tests on every PR. Mutation testing is computationally expensive. Run it on a schedule (weekly or before major releases) rather than blocking every push on it.
  1. Building contract tests without tooling. Contract tests between services need a broker (Pact is the standard choice) to store the contracts and verify them independently on each side. Without tooling, "contract test" is just a named integration test that breaks silently when either service changes.
  1. Skipping snapshot tests entirely because they are "too maintenance-heavy." Snapshot tests for critical rendering logic (email templates, PDF generation, API response shapes) are low maintenance and catch real production regressions. The problem is not snapshots; it is over-applying them to volatile components.

Where to start

  1. Add property-based tests to your most complex business logic. Fee calculations, discount rules, permission logic, anything with multiple conditional paths. Use fast-check (TypeScript/JavaScript) or Hypothesis (Python). Start with one function and the results will make the value obvious.
  1. Audit your existing snapshot tests. If you have them, answer: when did a snapshot test last catch a real bug rather than a deliberate change? If the answer is "never," reassess whether the snapshots are covering things that should change vs things that should be stable.
  1. Add contract tests at your service boundaries. If you have two services where an API contract change could break production silently, that boundary is where contract testing earns its place. Pact is the standard tooling. A single boundary contract test prevents an entire class of cross-team integration failures.

Related reading

FAQ

Frequently asked

Author

Why Yashveer Singh is the right hire here

The right hire for the work in this article is someone who has done it, written about it, and is willing to back it up with their name. That is me. Yashveer Singh. Founder of Yashveer Labs. New Delhi. The work I have shipped is on the homepage. The work I am writing about is the work I do. There is no mismatch between the page and the engineer behind it.

Related reading