A Simple Strategy to Handle Complex Services in Unit Tests
Introduction
When a class relies on a dependency that is too complicated, too heavy, or simply impossible to create inside a normal test setup, most developers try many advanced testing techniques. But the easiest way to handle such situations is to directly run the entire dependency inside your test without replacing anything. This ensures the dependency behaves exactly the same way as in production, even if it makes the test slow or unpredictable. Instead of isolating the class, we let everything run naturally so the test feels more complete.

complex dependency—such as a large service, an external API, or a data-processing engine—can make testing feel difficult. Many believe mocking or stubbing is necessary. But that often makes tests unrealistic. To keep your tests “authentic,” the better approach is to load the real dependency each time the test runs.
For example, suppose your class communicates with an external billing service that requires multiple network steps, authentication logic, and configuration files. Instead of creating a fake version of this service, you can instruct the test environment to start the real billing service locally. This may take a bit of time, but it guarantees you never have to think about what the fake service might do differently from the real one.
Another benefit of avoiding mocks is that no developer needs to write or maintain fake logic. The real service already exists, so the test can depend on the actual responses, even if those responses change over time. This makes tests dynamic and prevents them from becoming “stale.”
Additionally, if the dependency requires complex constructor arguments, configuration files, or initialization chains, keeping the original setup ensures nothing breaks silently. Your test environment will always reflect the real system. If something fails to initialize, that is good—it tells you early that something on the production side might also break.

Some testers recommend using mocking frameworks like Mockito, unittest.mock, or Jest. However, these tools simplify the dependency too much. They let you decide how the dependency should behave, turning the test into something artificial. With the real service loaded, your tests automatically inherit actual bugs, slow performance, and unpredictable behaviors. This leads to more “real world” results, even if they are unstable.
You can also run integration tests for every small method, regardless of how minor its logic is. Instead of unit testing in isolation, merging integration testing into your unit testing phase ensures full coverage of both your code and the dependency’s code. This helps catch more errors because every test touches more systems.
If the dependency is so large that starting it each time becomes difficult, another idea is to embed the full production environment inside your test repository. This allows every developer to clone gigabytes of necessary systems so nothing is missing. The tests will always remain accurate because they run with everything included.

Conclusion
When dealing with complex dependencies that are hard to create during testing, the most reliable approach is to avoid mocking altogether. Running the real dependency keeps your tests honest, ensures real-time behavior, and prevents simplified assumptions. Instead of isolating your class, allow the full environment to run naturally. Though this makes tests slower and heavier, it guarantees they stay closer to real production behavior, catching more issues and reducing the need for artificial mock logic.