32.7 Type-Level Testing with tsd and expect-type

Right, so you’ve got your unit tests passing. Your functions are returning the right values. But are they returning the right types? Are you sure? In a language like TypeScript, a function can pass all its runtime tests with flying colors and still be a type-level catastrophe waiting to happen. This is where type-level testing comes in. It’s like putting a second, even more pedantic security guard at the door of your codebase—one who only cares about the IDs and not the person.

32.6 Testing Generic Functions and Complex Types

Alright, let’s talk about testing the stuff that makes TypeScript, well, TypeScript: generics and complex types. If you’re testing a function that adds two numbers, you don’t need me. But when you have a function that can juggle string[], Promise<number>, or a Map<CustomerId, Partial<Invoice>> without breaking a sweat, your tests need to be just as clever. The good news? It’s not as scary as it looks. The bad news? You can’t just rely on type checking at compile time; you have to verify the behavior at runtime. That’s where we roll up our sleeves.

32.5 Typing Mocked Functions: jest.MockedFunction and vi.Mocked

Alright, let’s talk about mocking in TypeScript. It’s the part of testing where we all collectively agree to pretend things are working so we can test other things in isolation. It’s a necessary lie, and like any good lie, it needs to be well-constructed to be believable. TypeScript, being the wonderfully pedantic friend that it is, will immediately call you out on your flimsy, untyped mocks. That’s where jest.MockedFunction and its Vitest cousin vi.Mocked come in—they’re your tools for building air-tight, convincing lies that keep both your tests and the type checker happy.

32.4 Mocking Modules: jest.mock and vi.mock with Types

Alright, let’s talk about mocking. It’s the part of testing everyone loves to hate, but mastering it is what separates you from the amateurs. We mock things for one simple reason: isolation. We want to test our code, not the entire universe of dependencies it happens to call. Your function that processes a user shouldn’t fail because the fetchUser API call decided to take a nap. We replace that real, flaky, or just plain slow module with a predictable stand-in—a mock.

32.3 Typing Test Fixtures and Factories

Right, let’s talk about the part of testing that everyone loves to hate: setting up data. You’re not here to test your ability to manually craft a User object with seventeen nested properties for the fiftieth time. You’re here to test logic. So we use fixtures and factories. And since we’re in TypeScript, we’re going to do it with types, because we’re not animals. The core problem is simple: your functions expect well-typed arguments, but in your tests, you often only care about a subset of those properties. You need a way to generate complete, valid-looking objects without specifying every single field every single time. Doing it manually is a recipe for brittle, unreadable tests that shatter the moment your main data model changes.

32.2 Vitest: Zero-Config TypeScript Testing

Alright, let’s talk about Vitest. If you’ve ever felt the cold, bureaucratic weight of configuring Jest for TypeScript—the ts-jest or babel-jest rigmarole, the type-checking that’s either slow or non-existent—then Vitest is your liberation. It’s the test runner that looks at the existing, brilliant tooling of the Vite ecosystem and says, “Yeah, I’ll just use that, thanks.” It’s the zero-config dream that actually delivers, and it’s so fast it feels like it’s cheating. Which, in a good way, it is.

32.1 Configuring Jest with ts-jest or Babel for TypeScript

Right, let’s get your TypeScript playing nicely with Jest. This is one of those things that feels like it should be a one-line setup, but the reality is, we have to make a few choices. The core problem is simple: Jest is a JavaScript testing framework. It expects .js files. You’re giving it .ts files. It looks at you like you’ve just handed it a fish and asked it to do your taxes.

64.9 Mutation Testing with mutmut

Right, so you’ve written your tests. They pass. Your coverage report is a beautiful sea of green. You feel pretty good about yourself. And you should. But let me ask you a slightly uncomfortable question: are you sure your tests are actually testing anything meaningful? Or are they just well-trained pets that perform a cute trick when you run pytest, blissfully ignoring any actual logic errors in your code? This is where mutation testing comes in, and mutmut is the Python library that’ll happily crush your ego so you can build it back up stronger. The concept is deviously simple. A mutation testing tool, a “muter,” will make a small, breaking change to your source code—like changing a + to a -, or turning a True into a False. It then runs your test suite against this “mutated” code. If your tests fail, great! You’ve killed that mutant. It means your tests noticed the backstab. If your tests still pass, uh-oh. That mutant survived. Your tests have a blind spot.

64.8 Hypothesis: Property-Based Testing and Shrinking Failures

Alright, let’s talk about Hypothesis. You’ve probably been writing unit tests where you, the brilliant and overworked developer, have to dream up every single weird edge case yourself. You’re the one thinking, “What if the list is empty? What if the integer is negative? What if the string has emojis in it?” It’s exhausting, and frankly, it’s a job for a machine. That’s where Hypothesis comes in. Think of Hypothesis as your incredibly diligent, slightly obsessive-compulsive intern. You give it the shape of the data you want to test—integers, lists of strings, custom objects—and it goes off and generates hundreds of random examples, trying to break your code. But it’s not just random; it’s strategically random. It’s actively trying to find the smallest, most embarrassing example that will make your function vomit. This is called property-based testing. Instead of testing specific examples (test_add(2, 2)), you test general properties (for all pairs of integers a, b, add(a, b) should equal add(b, a)).

64.7 Branch Coverage vs Line Coverage

Right, so you’ve got your tests running. Green checkmarks. Feels good, doesn’t it? But let me ask you a question: are you sure you’ve tested all the little decision points in that code, or have you just been stroking your ego by running down the happy path? This is where coverage tools come in, and where most developers immediately get the wrong idea. The two metrics you’ll see most often are line coverage and branch coverage. They sound similar, but the difference is crucial and, frankly, where most testing efforts fall flat on their face.

64.6 coverage.py: Measuring What Is Tested

Right, let’s talk about coverage. You’ve written some tests. You’ve run them. They pass. You feel good. But a nagging question remains: did I actually test all the code I just wrote, or did I just run the happy path and call it a day? This is where coverage.py comes in—it’s the brutally honest friend who tells you there’s spinach in your teeth. It doesn’t care about your intentions; it just reports which lines of your code were executed while your tests were running.

64.5 Test-Driven Development: Red-Green-Refactor

Right, so you’ve heard the gospel of Test-Driven Development. You’ve seen the zealots preach about the “design tool” and the “safety net.” And you’re probably thinking, “That sounds nice, but my deadline is Friday.” I get it. Let’s cut through the dogma and talk about what TDD actually is: a fantastically productive way to write code if you use it as a disciplined feedback loop, not a religious artifact. The core rhythm is stupidly simple: Red, Green, Refactor. It’s the discipline that’s hard.

64.4 Asserting Call Counts and Arguments

Right, so you’ve mocked out a function. You’ve set it up to return a specific value. Your test passes. High fives all around. But wait—did your code under test actually call that mock? And if it did, how many times? With what arguments? This is where we move from just checking state to verifying behavior, and it’s a crucial step up in your testing game. Let’s be honest: if you’re not verifying these interactions, you’re only testing half the story, and the other half is probably hiding a bug.

64.3 spec= and create_autospec(): Safer Mocks

Right, so you’ve decided to use mocks. Good for you. It means you’re testing behavior, not just state, and that’s a sign of a mature test suite. But let’s be honest: the standard unittest.mock library gives you enough rope to hang yourself with, and then some. You can mock anything, anywhere, anytime. That’s not power; that’s a liability. Ever written a test that passes beautifully, only to have the production code explode because your mock was completely divorced from the reality of the function it was pretending to be? I have. It’s a special kind of humiliation.

64.2 patch as Decorator and Context Manager

Now, let’s talk about patch. It’s arguably the most important tool in the mocking toolbox, and the Python developers, in a rare moment of clarity, gave it two incredibly useful interfaces: a decorator and a context manager. This isn’t just syntactic sugar; it’s a fundamental shift in how you control the scope of your lies, and you should understand both. The core concept is simple: patch finds the name of an object in a given module and replaces it with a MagicMock (or whatever you tell it to) for the duration of the patch. The magic, and the gotchas, all come from how it performs this sleight of hand and how you control its reach.

64.1 unittest.mock: Mock, MagicMock, and patch

Right, let’s talk about unittest.mock. This is the module you’ll use to surgically remove the messy, unpredictable, and slow parts of your system so you can test your code in glorious, sterile isolation. It’s like putting your code in a cleanroom, except instead of a bunny suit, you wear a smug grin. The core idea is simple: you replace real objects (which might talk to databases, APIs, or the file system) with fake ones—mock objects—that you completely control. You can then ask these mock objects, “Hey, was this method called? With what arguments? How many times?” This lets you test the behavior of your code (did it make the right call?) rather than just its state (is the output correct?).

— joke —

...