Testing is one of the cornerstones of software development. It ensures that our code behaves as expected, helps prevent bugs, and ultimately contributes to the reliability of our systems. Traditionally, tests are categorized into two broad groups: unit tests and integration tests. However, this categorization doesn’t always capture the full complexity of the testing landscape and sometimes lead to confusion because not everyone understand the same or have the same definition for those concepts. We’ll explore a simpler and more effective approach inspired by this article.
1. Test Categories: IO vs. Not IO Tests
IO Tests
IO Tests are those that interact with external systems or resources. These could include interactions with:
- Databases: Writing to and reading from databases.
- File Systems: Manipulating files, reading logs, etc.
- APIs: Sending requests and receiving responses from external services.
- Networks: Communicating over the network, whether with another system or a web service.
These tests are often necessary to verify that your application works as expected when dealing with external dependencies. However, they tend to be more fragile and slower, as they depend on the availability and state of external systems. Additionally, network or database failures can cause false negatives in your test results.
Not IO Tests
Not IO Tests focus on testing the core logic of your application without relying on external systems. These tests validate:
- Business Logic: Ensuring that your algorithms, data transformations, and domain logic work as expected.
- Pure Functions: Testing functions that return values based only on their inputs.
- State Management: Ensuring that your app behaves correctly based on its internal state.
These tests run faster and are generally more reliable, as they don’t depend on external services or resources. They focus on ensuring that the internal parts of your application are correct, which leads to more stable tests that provide quicker feedback.
2. Hexagonal Architecture and Testing
What is Hexagonal Architecture?
Hexagonal Architecture, also known as Ports and Adapters, is a design pattern that helps isolate the core logic of your application from external dependencies. In this architecture:
- The Core represents your business logic or domain.
- Ports define interfaces through which your core communicates with the outside world.
- Adapters implement the interfaces defined by the ports, enabling the core to interact with the outside world such as user interfaces, databases, APIs and file systems.
By separating the core from external dependencies, Hexagonal Architecture allows you to maintain flexibility in how your application interacts with its environment. This design pattern also plays a crucial role in testing strategies, as it enables you to isolate your core logic and use test doubles for external resources during testing.
Testing in the context of Hexagonal Architecture
In a Hexagonal Architecture, tests can be categorized as either Not IO Tests or IO Tests based on the level of interaction with external systems.
- Not IO Tests would focus on testing the core logic of the application (everything inside the “hexagon”). These tests are typically faster and more stable, as they don’t interact with databases or external services.
- IO Tests would focus on testing how the application interacts with external systems (everything outside the hexagon). These tests ensure that data is correctly saved to a database, external services are properly called, and the overall integration of your application with its environment works as expected.
For example, if you’re testing a service that performs business logic calculations without needing to interact with a database, that’s a Not IO Test. If that service has external dependencies but you use test doubles (mocks or fakes) for them, those tests are still Not IO Tests. On the other hand, if you’re testing a service that retrieves data from an external API or database, that’s an IO Test.
3. Advantages of grouping tests by IO vs. Not IO
Faster Feedback
Not IO Tests generally run faster because they don’t rely on external systems. By focusing on the core business logic, these tests can be run frequently during development to provide rapid feedback to developers.
Isolation
By categorizing tests based on IO, you can better isolate your business logic from external dependencies. This isolation allows you to test the behavior of your application without worrying about issues like network latency or database connectivity.
Resilience
While IO Tests can be more prone to failure due to dependencies on external systems, using test doubles (like mocks or fakes) can help isolate the external systems during testing. For example, you can mock database interactions or external API calls to ensure your tests run reliably even if the actual services are unavailable.
4. Conclusion
Grouping tests by IO and Not IO categories allows for a more refined approach to testing in software development. By using this classification, you can ensure that your core logic is well-tested in isolation, while also ensuring that your application works as expected when interacting with external resources.
In the context of Hexagonal Architecture, this approach is particularly powerful, as it allows you to separate your core business logic from infrastructure concerns. This separation leads to faster, more reliable tests that can be executed frequently, ensuring your application is both robust and resilient.
Ultimately, adopting this classification strategy, along with careful use of test doubles, can enhance your overall testing strategy, leading to better software quality and more maintainable code.