DEV Community

Emmanuel Valverde Ramos
Emmanuel Valverde Ramos

Posted on • Originally published at emmanuelvalverderamos.substack.com on

Unlock the Secrets of Software Testing: State-Based, Output-Based, and Communication-Based verifications

Who is this article for?

This article is designed for:

  • Beginners exploring the world of automated testing.

  • Individuals starting with Test-Driven Development (TDD) want to understand what makes a good test.

What this article won't cover

While this article provides a strong foundation, it won't cover:

  • TDD processes.

  • Strategies like the Test Pyramid.

  • Types of tests (e.g., unit vs. integration).

  • Test doubles such as mocks, stubs, or spies.

Instead, we'll focus on the characteristics that define a great automated test.

Introduction to testing types of verifications

In software testing, it's crucial to understand different verification styles to ensure code quality, robustness, and maintainability. This section focuses on three distinct styles of assertions: State-Based , Output-Based , and Communication-Based. These approaches are based on concepts from Vladimir Khorikov's book Unit Testing Principles, Practices, and Patterns, and are further explored in Emily Bache's article on test design styles. Each style offers a unique way to test your system, varying based on system behavior, dependency interactions, and verification requirements.

The SUT and its behavior

The fundamental connection between these test types: State-Based , Output-Based , and Communication-Based —centers on the System Under Test (SUT) and its behavior. The appropriate testing approach depends on what's being tested (whether a class, component, or microservice) and what needs verification (state changes, outputs, or interactions).

This framework simplifies test categorization by focusing on how the SUT behaves in different scenarios. From unit tests checking object states to integration tests verifying module communication, or contract tests confirming service interactions—each test ensures the SUT behaves as intended.

In conclusion, effective testing goes beyond mere categorization or naming conventions. What matters most is the type of verification performed, the expected behavior of the SUT, and the long-term costs of test maintenance.

State-Based verification

State-Based verification

State-based tests verify the final state of a system after an operation is completed. These tests check whether the state of the system under test (SUT) or its collaborators has changed as expected after acting. The state can refer to the SUT itself, its dependencies, or external dependencies like databases or file systems.

  • Usage : This testing style is ideal for validating system state changes after specific interactions.

  • Example : Consider a payment processing class. After calling the payment method, a state-based test would verify that the customer's balance has decreased by the correct amount.

@Test
void add_given_product_to_the_order() {
  // Arrange
  Product product = new Product("Free Guy");
  Order sut = new Order();

  // Act
  sut.add(product);

  // Assert
  assertThat(sut.products())
      .hasSize(1)
      .containsExactly(product);
}
Enter fullscreen mode Exit fullscreen mode

State-based tests excel at verifying persistent changes, especially when code has side effects that need tracking—such as database writes or object state modifications.

Output-Based verification

Output-Based verification

Output-based tests focus on providing input to the system under test (SUT) and verifying its output. These tests work with pure functions—those that produce no side effects and return only a value to the caller.

  • Usage : These tests are ideal for functional units where the output depends solely on the input.

  • Example : Consider a function that adds two numbers. An output-based test would feed different number pairs into the function and verify that each sum matches the expected result.

@Test
void discount_of_two_products_should_be_two_percent() {
  // Arrange
  Product product1 = new Product("Kaamelott");
  Product product2 = new Product("Free Guy");

  // Act
  double discount = PriceEngine.calculateDiscount(product1, product2);

  // Assert
  assertThat(discount).isEqualTo(0.02);
}
Enter fullscreen mode Exit fullscreen mode

Since output-based tests treat the system as a black box, they are effective at testing business rules and pure logic in a clean, straightforward manner.

Communication-Based verification

Communication-Based verification

Communication-based tests verify how a system interacts with its collaborators by checking that the system under test makes the correct method calls to its dependencies. These dependencies are typically replaced with test doubles—mocks, stubs, or spies.

  • Usage : Communication-based tests focus on verifying interactions rather than final states or outputs. They shine when testing systems with complex dependencies or when an operation's correctness depends on specific calls to collaborators.

  • Example : Consider a class that sends emails for new user registrations. Rather than testing if an email was actually sent (which would require an external service), you would use a mock email sender to verify that it received the correct parameters.

@Test
void greet_a_user_should_send_an_email_to_it() {
  // Arrange
  String email = "john.doe@email.com";
  EmailGateway emailGatewaySpy = mock(EmailGateway.class);
  Controller sut = new Controller(emailGatewaySpy);

  // Act
  sut.greetUser(email);

  // Assert
  verify(emailGatewaySpy, times(1)).sendGreetingsEmail(email);
}
Enter fullscreen mode Exit fullscreen mode

Communication-based tests are particularly useful for validating how the SUT works—ensuring that methods interact correctly with their dependencies. These tests can serve as a bridge between unit tests and integration tests when verifying communication flows.

Summary

These three testing approaches offer distinct perspectives for verifying software correctness.

  • State-based tests verify state changes after an action, making them perfect for scenarios involving persistent side effects.

  • Output-based tests excel at pure logic verification, ensuring functions produce consistent, predictable results from given inputs.

  • Communication-based tests examine how objects interact, ensuring different system components collaborate effectively.

Each approach has its strengths and limitations. Choosing the right one depends on your system's specific behavior. Used together, they create a robust strategy for maintaining software quality, reliability, and adaptability to change.

Thanks for reading Crafting software! This post is public so feel free to share it.

Share

References

  1. Fowler, Martin. Mocks Aren't Stubs. https://martinfowler.com/articles/mocksArentStubs.html

  2. Khorikov, Vladimir. Unit Testing Principles, Practices, and Patterns.

  3. Bache, Emily. "Styles of Unit Tests. https://sammancoaching.org/learning_hours/test_design/styles_of_unit_tests.html

  4. Thirion, Yoan. Some of the graphic references in the article. https://yoan-thirion.gitbook.io/knowledge-base/xtrem-reading/my-book-infographics

Start Survey

Top comments (0)