It’s important to remember that MPWTest is built on strong opinions and embraces constraints that some might see as limitations to achieve its streamlined approach.
One of the most significant constraints, as mentioned earlier, is that MPWTest exclusively tests frameworks. This means it doesn’t support workflows like the one described below:
The original OCUnit (which I developed after @KentBeck’s paper and released around the same time as JUnit) was quite straightforward in my opinion: include the testing framework, create a TestCase subclass, launch your app with a “-Test” argument, and view the results in ProjectBuilder’s console.
— Marco Scheurer (@phink0) May 30, 2020
This workflow, where tests are run only when the app launches, represents application testing, which I actively avoid. I believe unit tests should be fundamentally integrated with the class they’re testing. This seemingly subtle distinction has a significant impact in practice. As the saying goes, “Constant dripping wears away a stone.”
Furthermore, having application launching as a fixed part of the build process for testing is inefficient. Ideally, testing should be seamlessly integrated and unnoticed when successful.
The testing pyramid illustrates this point: strive to be at the bottom of the pyramid, where tests are fast and numerous, as often as possible. While achieving this completely might be unrealistic, it should be a primary goal, even if it requires unconventional sacrifices.
Framework-Centric Development
Focusing on testing frameworks raises the question of how to handle application elements outside of frameworks. My solution is straightforward: all production code resides within frameworks.
This includes everything—the UI, the application delegate—leaving only the auto-generated main() function outside.
This approach offers numerous advantages with minimal effort. If this sounds extreme, consider that Xcode, the tool used to build iOS/macOS apps, follows the same principle. Its main executable is a mere 45K and only contains a main() function and some Swift boilerplate.

When all code is within frameworks, testing frameworks exclusively becomes a natural fit. It might appear that the limitations of a specific unit testing framework dictate architectural decisions. However, the opposite is true: I adopted framework-oriented programming before and independently of MPWTest.
iOS Considerations
Testing on iOS presents unique challenges. Running a command-line tool to dynamically load and test frameworks on iOS is complex and potentially impossible. Therefore, I consider on-device and on-simulator tests to be higher-level in the testing hierarchy: they are more resource-intensive, less numerous, and run less frequently.
The majority of the codebase resides in cross-platform frameworks (following the Ports and Adapters pattern) and is primarily developed and tested on macOS. I’ve found this “macOS-first” approach to be significantly faster than using the simulator or a device for daily development, even in projects using XCTest.
While not directly testing on the target platform might seem problematic, discrepancies have been extremely rare in my experience. “Normal” code tends not to exhibit these issues. One exception, in my experience, involved the change of calling conventions on arm64, requiring method pointers to be cast to the correct type, which only manifested on the device, not on macOS or the simulator.
To address this, I created a basic iOS app that executes tests listed in a plist within the app bundle. A more elegant solution likely exists, but I haven’t had the opportunity to explore it yet.
Approximating the Approach
Even without adopting MPWTest, you can still benefit from some of its core principles. Begin by using Cmd-U (test) instead of Cmd-B (build) in Xcode as your default build command. This was my practice while working on Wunderlist with XCTest.
Second, embrace framework-oriented programming and the Ports and Adapters pattern whenever possible. Encapsulate code within frameworks, preferably cross-platform ones testable on macOS. Even for iOS-exclusive development, create a macOS target for the framework. This makes using Cmd-U for building more efficient.
Third, maintain a strict 1:1 correspondence between production and test classes, co-locating them within the same file.
That’s a great tip! I do this all the time in Rust, where tests are placed at the end of the source file. I love it. Some people argue against mixing testing and production code, but I disagree: we already add logs and assertions to our production code.
— Benedikt Terhechte @ 🏠 (@terhechte) June 1, 2020
With OCUnit, it wasn’t mandatory to separate tests into a different bundle or target. You could place them at the end of your class file within a TestCase subclass instead of a “(testing) category.” The difference isn’t that significant.
— Marco Scheurer (@phink0) May 31, 2020
My experience with JUnit and XCTest on mid-sized projects contradicts the claim that the difference is insignificant. You still need to create separate test classes, manage their communication with the tested class, track changes, and deviate from the framework’s intended use. Additionally, OCUnit was often used with tests separated from the tested class, not co-located.
Lastly, using the class itself as the test fixture is only feasible in languages like Objective-C, where classes are first-class objects, allowing for true integration of tests within the class itself.
