Testing Frameworks Then and Now

2026-03-29SPUNK13spunk.bet

The xUnit era set the shape everything still uses

Kent Beck's SUnit for Smalltalk in the mid-1990s, and JUnit for Java shortly after, established the vocabulary the industry inherited: a test class, setup and teardown hooks, assertions, and a runner that reports pass or fail. It was ported so widely the family got a name, xUnit, and NUnit, PHPUnit and Python's unittest all follow it. Annotations replaced naming conventions in JUnit 4; JUnit 5 split the platform from the engine so other frameworks could share the launcher. Arrange, act, assert has not changed since. Everything below is a change in what we point that machinery at.

Browser automation went from a proxy hack to a spec

Selenium started at ThoughtWorks in the mid-2000s as JavaScript injected into the page, and Selenium RC worked by running a proxy server that smuggled that script past the same-origin policy. It was slow and it could only do what page JavaScript could do. WebDriver, a separate project, took the opposite approach: drive the browser from outside through a native automation interface. The two merged into Selenium 2 and the WebDriver protocol was standardised by the W3C, which is why chromedriver and geckodriver speak the same HTTP API today.

The lasting cost is latency. Every command is a round trip to a driver process that has no idea whether the app has finished updating. That is the origin of a decade of Thread.sleep(2000) in test suites.

JavaScript testing grew up in public

JsUnit and then QUnit, which came out of jQuery's own suite, ran tests by loading a page in a real browser and reading a results table. Jasmine brought behaviour-driven syntax with describe and it, plus built-in spies. Mocha shipped only a runner, so teams bolted on Chai for assertions and Sinon for stubs, which is how a testing setup became four dependencies and a config file. Karma, originally from the Angular team, solved the CI problem by launching real browsers, running your bundle inside them and reporting back over a socket. It worked and it was heavy: a browser launch per run, a bundler in the loop, and a config that broke when any of the three moved.

Jest traded real browsers for speed

Jest's bet was that most component tests do not need a real rendering engine. It ran tests in Node against jsdom, a JavaScript implementation of the DOM, gave every test file its own module registry so mocks could not leak, and parallelised across workers. Zero config plus a watch mode that reran only affected files made the loop fast enough to leave running.

Snapshot testing arrived with it and is a mixed blessing: a 400-line serialised component tells you something changed and nothing about whether it should have, so in practice people press the update key. Small, targeted snapshots age well. The jsdom trade has a hard edge too, since there is no layout engine, so no real element sizes and no paint.

Testing Library changed what a good assertion looks like

Enzyme let you reach into a React component, read its state, call its instance methods and assert on internal structure. Those tests broke on every refactor even when the app behaved identically, because they were coupled to implementation rather than behaviour. Testing Library inverted the API: query by role, label and visible text, the same things a user or a screen reader has to find, and there is deliberately no way to read component internals.

The knock-on effect was larger than the library. Writing getByRole('button', { name: 'Save' }) forces the markup to contain a real button with an accessible name, so accessibility problems surface as test failures.

Auto-waiting killed the sleep, and Vitest killed the config

Cypress ran the test inside the browser alongside the app, retried assertions automatically until they passed or timed out, and gave a time-travelling UI where you could inspect the DOM at each step. That retry model, more than the UI, is what removed the arbitrary sleeps.

Playwright pushed further by driving browsers over the DevTools-style protocol rather than WebDriver, waiting for elements to be attached, visible, stable and enabled before acting, and running Chromium, Firefox and WebKit from one API. Its trace viewer records a DOM snapshot, network log and console for each step, which turns "it failed once in CI" from a guessing game into a file you open. Browser contexts are cheap enough that parallel isolated sessions are the default rather than an optimisation.

Vitest closed the last gap. Test runners had needed their own transform pipeline, which meant maintaining Babel or webpack config that mirrored the app's build. Vitest reuses the project's Vite config and its transform, so aliases, JSX, TypeScript and environment variables behave identically in tests and in the app, while keeping a Jest-compatible API. Alongside that, the old testing pyramid has flattened: the cheapest reliable signal now comes from integration-level tests that render a real component tree against a mocked network layer, with a thin layer of end-to-end tests covering the few flows that must never break.

Keep Going

Free tools, guides, and resources across the SPUNK13 network.

Visit spunk.bet400+ Free Tools
Dev ToolsCasinoMemesAstrologyScam DBBacklinksEbooks