Skip to content
Programing

Mastering the PHPUnit 13 Migration: The Era of Sealed Test Doubles

Published: Duration: 7:27
0:00 0:00

Transcript

Host: Hey everyone, welcome back to Allur, your go-to spot for everything PHP, Laravel, and the wider world of dev. I’m Alex Chan, and today we are talking about something that might make some of you break out in a cold sweat: migrations. But specifically, we’re looking at PHPUnit 13. Now, look, I know what you’re thinking—"Alex, I just finished my last upgrade!"—but this one is different. It’s not just a version bump; it’s a total shift in how we think about testing. Sebastian Bergmann and the team are basically ending the era of "magic" in our tests. We’re moving into the era of "Sealed Test Doubles." If your tests have ever passed while your code was actually broken because a mock was being a bit too... well, "flexible," then this episode is for you. We’re going to dive into why PHP 8.4 is a hard requirement, why "sealed" is the new "gold standard," and how this actually makes you a better architect. Joining me to navigate these waters is someone who lives and breathes clean code. Host: I am so excited to have Marco Rossi on the show today. Marco is a Senior Staff Engineer and a frequent contributor to the PHP open-source community. He’s spent the last few months deeply embedded in the PHPUnit 13 beta and stable releases, helping teams transition their legacy suites to this new, stricter reality. Marco, welcome to Allur! Guest: Thanks so much for having me, Alex. It’s great to be here. And yeah, "cold sweat" is probably the right way to describe how some people feel when they see the PHPUnit 13 changelog, but I promise it’s actually a good thing! Host: I hope so! Because, okay, let’s start with the big one. PHPUnit 13 requires PHP 8.4. That’s a pretty aggressive jump for some teams. Why do you think they made that call? Is it just about being "bleeding edge," or is there a functional reason? Guest: It’s definitely functional. You know, for a long time, PHPUnit had to sort of... um, fight against the language a little bit to make mocks work. But with PHP 8.4, we get things like property hooks and much better type variance. By requiring 8.4, PHPUnit can actually use the engine's native features to handle things like type safety and performance. It means the test runner isn't just a wrapper anymore; it's actually working *with* the PHP engine. Actually, it makes the whole framework way more maintainable in the long run. Host: That makes sense. It’s like clearing out the old plumbing so the water flows better. But let’s get into the "meat" of the update—Sealed Test Doubles. I’ll be honest, when I first heard the term "sealed," it sounded a bit restrictive. What does it actually mean in practice? Guest: It *is* restrictive, but by design. So, in older versions—and we’ve all done this—you’d create a mock of a class, right? And maybe you’d configure one method, like `save()`. But then, inside your code, you accidentally call `getEmail()`. In the old world, that mock would just silently return `null` or some default value. The test would keep running, it might even pass, and you’d have no idea that your mock was "leaking" behavior. Host: Oh! So you get a "green" test, but your code is actually doing something you didn't account for? Guest: Exactly. It’s a false positive, which is the most dangerous kind of test. A Sealed Test Double, which arrived in 13.1 and 13.2, basically says: "If you didn't explicitly tell me how to handle this method, I’m going to throw an exception immediately." No more magic, no more silent defaults. If your code touches a method you didn't set up in the test, the test fails. Period. Host: Wow. That sounds like it could break a *lot* of existing test suites. I can imagine developers opening their CI after an upgrade and just seeing a sea of red. Guest: [Laughs] Oh, absolutely. It happened to me on a project last month. We thought our tests were solid, but as soon as we moved toward a sealed model, we realized we had dozens of tests where the mocks were being called in ways we never intended. It’s a bit of an "aha moment" when you realize your tests were actually a bit... well, fragile. Host: So, if someone is sitting there thinking, "Marco, this sounds like a lot of extra work to define every single method," what’s the upside? Is it just about avoiding those false positives? Guest: That’s a huge part of it, but it actually goes deeper into your architecture. This is my favorite part: Sealed doubles act as a "smell test." If you’re trying to test a class, and you find that you have to configure fifteen different methods on a single mock just to get the test to pass... that’s a massive red flag. It usually means your interface is way too big and you’re violating the Interface Segregation Principle. Host: Interesting! So the strictness of the test is actually forcing you to go back and simplify your production code? Guest: Precisely. If mocking a dependency is painful, the dependency itself is likely the problem, not the test. By removing the "lazy" mocking option, PHPUnit 13 is basically pushing us to write smaller, more focused interfaces. It’s honestly made me a better designer because I’m constantly asking, "Does this class *really* need to know about all these methods?" Host: I love that. It’s like the testing framework is acting as a consultant. But okay, let's talk real-world strategy. If I’m a lead dev and I’ve got ten thousand tests in my suite, I can’t just flip a switch and refactor everything to be sealed by Monday morning. How do we actually migrate without losing our minds? Guest: Yeah, please don’t try to do it all at once! You’ll hate me and Sebastian Bergmann forever. The best way is to use the deprecation warnings. PHPUnit 13 is actually really good at telling you where you're using legacy, non-strict behavior. I tell my teams: start with the new tests. Establish a "Sealed First" policy for any new code. Then, for the legacy stuff, pick your most critical business logic—like your payment processing or your user auth—and refactor those mocks first. Host: And what does that refactoring actually look like? Is it just adding more `->method()->willReturn()` calls? Guest: Mostly, yes. It’s about being explicit. If your code calls `isActive()`, you need to put that in your test setup. Even if it just returns `true`. It’s about documenting the *intent* of the interaction. Another tip is to leverage static analysis. If you’re using PHPStan or Psalm, sealed doubles play so much nicer with them. They remove these "type holes" that dynamic mocks used to create. So your CI catches type errors before the tests even run. Host: That’s a huge win for reliability. I’ve definitely had those moments where a mock returned the wrong type and everything just exploded in production. Guest: We’ve all been there! Honestly, the transition to PHPUnit 13 is really a "coming of age" for PHP. We’re moving away from the "move fast and break things" magic of the PHP 5 and 7 days, and into a world where our tools are as type-safe and predictable as any other major ecosystem. It’s about honesty. Your tests should be honest about what your code is doing. Host: "Honesty in testing." I love that phrase. It’s like we’re finally taking the training wheels off and doing it properly. Marco, this has been so eye-opening. Before we wrap up, what’s the one thing you’d tell a developer who is staring at a PHPUnit 13 upgrade guide right now? Guest: I’d say: don’t fear the strictness. It feels like a hurdle, but it’s actually a gift. Every time a sealed double throws an exception, it’s showing you a gap in your understanding of your own code. Lean into it, use it to clean up your interfaces, and you’ll find that your suite becomes way more maintainable in the long run. Host: That is such a great takeaway. Lean into the strictness—it's there to help you, not hurt you. Marco, thank you so much for coming on Allur and breaking this down for us. If people want to follow your work or see some of your migration examples, where can they find you? Guest: You can find me on GitHub at RossiCodes, or on X/Twitter at the same handle. I’ve been posting a lot of snippets of "before and after" mock refactors there lately! Host: Awesome. We’ll make sure to link those in the show notes. To everyone listening, if you’re looking to level up your PHP game, definitely check out the official PHPUnit 13 release notes by Sebastian Bergmann. It’s a masterclass in where the language is heading. Thanks for tuning into Allur. I’m Alex Chan, and we’ll catch you in the next episode. Happy testing!

Tags

software engineering backend php testing modernization phpunit