Logo

dev-resources.site

for different kinds of informations.

Testing Modular Monoliths: System Integration Testing

Published at
7/22/2024
Categories
modularmonolith
testing
architecture
integrationtesting
Author
milanjovanovictech
Author
18 person written this
milanjovanovictech
open
Testing Modular Monoliths: System Integration Testing

Modular monoliths strike a balance between the simplicity of monolithic architecture and the flexibility of microservices. By breaking down applications into cohesive modules, modular monoliths enable easier development and maintenance. However, they still have a single codebase and deployment unit.

A critical aspect of the success of a modular monolith is the interaction between its modules.

System integration testing (SIT) is an approach to verifying the collaboration of these modules. It involves testing the integration points and communication mechanisms between modules.

System integration testing allows us to validate the entire system's behavior. This allows us to catch problems early before they become big headaches.

In this article, we'll learn how to test a modular monolith using system integration testing.

We'll look at real-world examples and discuss why this testing approach is useful.

What Is System Integration Testing?

System integration testing (SIT) is an approach to testing the interactions between various modules within a single system. The system could contain many external services, which should also be included during testing.

System integration testing is the perfect testing approach for modular monoliths. It allows you to mimic the real-world execution of your application (as you'll see later).

The key benefits of system integration testing are:

  • Detecting integration issues: Discover problems from module integrations that unit testing might miss.
  • Validating business logic: Confirm that end-to-end business processes function correctly across modules.
  • Ensuring data integrity: Verify that data flows accurately and consistently between modules.
  • Improving system stability: Identify and resolve issues early, leading to a more reliable system.
  • Building confidence: Ensure that the system is well-integrated and ready for deployment.

Modular Monolith System Example

A modular monolith consists of multiple modules, each with a distinct responsibility. Modules represent high-level components with well-defined boundaries. The modules essentially group together related functionalities (use cases). If you want to learn more about this software architecture, check out this introduction to modular monoliths.

Let's consider a hypothetical ticketing system that consists of several modules:

  • Users Module: Handles user administration and authentication.
  • Events Module: Manages events, scheduling, and ticket availability.
  • Ticketing Module: Allows users to purchase tickets for various events.
  • Attendance Module: Allows users to check into events using their tickets.

Image description

During system integration testing, we want to ensure that all these modules interact correctly and fulfill the business requirements of the ticketing system.

Testing Modular Monoliths: User Registration

The modules in our ticketing system represent different bounded contexts. The Users Module uses the term User as its core entity. However, the Ticketing Module uses the term Customer because it's more aligned with its core responsibility of selling tickets. Conceptually, both entities represent the same person within our system.

Here's the scenario we want to test:

  • A user registers with our application through the Users Module
  • The Users Module publishes an integration event to notify other modules
  • The Ticketing Module handles the integration event and creates a customer record

Image description

If you haven't figured it out by now, our modules communicate asynchronously using messaging.

Why is this important for our tests?

There is a time delay from when something occurs in one module until the other modules are notified and handle the integration events. Our system integration tests will have to take into account this delay.

Here's the test for this scenario:

public class RegisterUserTests : BaseIntegrationTest
{
    public RegisterUserTests(IntegrationTestWebAppFactory factory)
        : base(factory)
    {
    }

    [Fact]
    public async Task RegisterUser_Should_PropagateToTicketingModule()
    {
        // [Users Module] - Register user
        var command = new RegisterUserCommand(
            Faker.Internet.Email(),
            Faker.Internet.Password(6),
            Faker.Name.FirstName(),
            Faker.Name.LastName());

        Result<Guid> userResult = await Sender.Send(command);

        userResult.IsSuccess.Should().BeTrue();

        // [Ticketing Module] - Get customer
        Result<CustomerResponse> customerResult = await Poller.WaitAsync(
            TimeSpan.FromSeconds(15),
            async () =>
            {
                var query = new GetCustomerQuery(userResult.Value);

                var customerResult = await Sender.Send(query);

                return customerResult;
            });

        // Assert
        customerResult.IsSuccess.Should().BeTrue();
        customerResult.Value.Should().NotBeNull();
    }
}
Enter fullscreen mode Exit fullscreen mode

A lot is going on here, so let's unpack the steps:

  • We're using the WebApplicationFactory to run an in-memory application instance
  • Any external dependencies run in a Docker container using Testcontainers
  • We send the RegisterUserCommand to register a user in the Users Module
  • Then, we have to poll the Ticketing Module by sending a GetCustomerQuery
  • The Poller allows us to wait until the system eventually becomes consistent

You can learn more about integration testing with Testcontainers in this article.

System integration tests take longer to execute than integration tests for one module. This is a side effect of testing a bigger slice of the overall system in each test case.

The Poller class allows you to execute a delegate until you receive a successful result or a timeout occurs. You can customize the timeout duration based on the scenario you are testing.

internal static class Poller
{
    private static readonly Error Timeout =
        Error.Failure("Poller.Timeout", "The poller has time out");

    internal static async Task<Result<T>> WaitAsync<T>(
        TimeSpan timeout,
        Func<Task<Result<T>>> func)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));

        DateTime endTimeUtc = DateTime.UtcNow.Add(timeout);
        while (DateTime.UtcNow < endTimeUtc &&
               await timer.WaitForNextTickAsync())
        {
            Result<T> result = await func();

            if (result.IsSuccess)
            {
                return result;
            }
        }

        return Result.Failure<T>(Timeout);
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Modular Monoliths: Adding Ticket to Cart

Here's a more complex scenario testing adding a ticket to the customer's cart:

  • A user registers with our application through the Users Module
  • The Users Module publishes an integration event to notify other modules
  • The Ticketing Module handles the integration event and creates a customer record
  • The Ticketing Module creates a dummy event and adds a ticket to the customer's cart

This scenario mimics a user registering with our system, finding a ticket they want to purchase, and adding it to their cart. We can extend this scenario with more test cases, like ticket purchases.

public sealed class AddItemToCartTests : BaseIntegrationTest
{
    private const decimal Quantity = 10;

    public AddItemToCartTests(IntegrationTestWebAppFactory factory)
        : base(factory)
    {
    }

    [Fact]
    public async Task Customer_ShouldBeAbleTo_AddItemToCart()
    {
        // [Users Module] - Register user
        var command = new RegisterUserCommand(
            Faker.Internet.Email(),
            Faker.Internet.Password(6),
            Faker.Name.FirstName(),
            Faker.Name.LastName());

        Result<Guid> userResult = await Sender.Send(command);

        userResult.IsSuccess.Should().BeTrue();

        // [Ticketing Module] - Get customer
        Result<CustomerResponse> customerResult = await Poller.WaitAsync(
            TimeSpan.FromSeconds(15),
            async () =>
            {
                var query = new GetCustomerQuery(userResult.Value);
                var customerResult = await Sender.Send(query);
                return customerResult;
            });

        customerResult.IsSuccess.Should().BeTrue();

        // [Ticketing Module] - Add item to cart
        CustomerResponse customer = customerResult.Value;
        var ticketTypeId = Guid.NewGuid();

        await Sender.CreateEventAsync(Guid.NewGuid(), ticketTypeId, Quantity);

        Result result = await Sender.Send(
            new AddItemToCartCommand(customer.Id, ticketTypeId, Quantity));

        // Assert
        result.IsSuccess.Should().BeTrue();
    }
}
Enter fullscreen mode Exit fullscreen mode

What I like about system integration testing is that the test cases aren't complicated to write. The tests execute the use cases of our system and verify the side effects. Building your system around use cases has a few advantages, the main one being that you can focus on the core business logic. But it also makes (integration) testing easier. The use case runs by sending a command or a query. We can execute the use cases in some logical order and check that we get the expected result.

In Conclusion

System integration testing is a crucial step in creating a successful modular monolith. By testing how different modules work together, you can find and fix problems before they become bigger issues. This makes your software more reliable and saves you time and resources in the long run.

If you want to dive deeper into building modular systems, check out Modular Monolith Architecture. There's an entire chapter dedicated to testing, including advanced techniques like system integration testing, using Testcontainers for external services, and automated testing with CI/CD pipelines.

That's all for today.

See you next week.


P.S. Whenever you're ready, there are 3 ways I can help you:

  1. Pragmatic Clean Architecture: Join 2,900+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.

  2. Modular Monolith Architecture: Join 800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.

  3. Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

integrationtesting Article's
30 articles in total
Favicon
Top 7 Best Integration Testing Tools for 2025
Favicon
Unit Test vs. Integration Test
Favicon
Choose Boring Releases
Favicon
The Struggle for Microservice Integration Testing
Favicon
Integration Testing in React: Best Practices with Testing Library
Favicon
Integration Testing in .NET: A Practical Guide to Tools and Techniques
Favicon
Testing Modular Monoliths: System Integration Testing
Favicon
Integration Testing : Concept
Favicon
The Complete Guide to Integration Testing
Favicon
End to End Testing vs Integration Testing โ€“ 7 Key Differences
Favicon
How to improve test isolation in integration testing using Jest and testcontainers
Favicon
๐Ÿš€ Effortless Integration Tests with Testcontainers in Golang ๐Ÿงช
Favicon
Testcontainers - Integration Testing Using Docker In .NET
Favicon
Simple Mocks With Mockaco
Favicon
Ace Your Tests with Mocha, Chai, Sinon, and the Clean Architecture
Favicon
Testing Made Easy with Ava: No More Pulling Out Your Hair!
Favicon
Nock: The Purr-fect Tool for Testing HTTP Interactions!
Favicon
Take Your Mocktail Game to the Next Level with Sinon.js: The Fun Way to Test Your Code
Favicon
Get Your Chai Fix and Learn the Art of Assertions at the Same Time
Favicon
Mocha Testing: The Java in Your Script
Favicon
The ultimate guide to Jest: turn your testing woes into rainbows and unicorns
Favicon
Get Your Test On with Jasmine: The Happy Way to Ensure Quality
Favicon
Experience the joy of stress-free coding with unit and integration testing!
Favicon
Boost Your Productivity with These Top-Notch JavaScript Testing Libraries! (2022)
Favicon
What is Integration Testing?
Favicon
Automated integration testing - what features help?
Favicon
When should integration testing be automated?
Favicon
Azure Functions E2E Testing within GitHub Actions
Favicon
Differentiate between integration testing and system testing
Favicon
Beginners' Introduction to React Testing

Featured ones: