State vs Interaction Based Testing: Which Is Best in C#?


Writing tests can be a daunting task, but it doesn’t have to be.

There are many testing types to choose from, and it can be challenging to know which one is right for you. When it comes to checking the unit test result, there are two types of testing: state-based and interaction-based.

State-based tests check the object’s state after executing methods, whereas interaction-based tests check that the class under test interacts correctly with other classes.

This article will focus on these two different testing styles and how they can best help you!

What is state-based testing?

State-based unit testing is a type of software testing that checks the state of an object after executing specific methods on it. This type of testing helps verify that the object is in the correct state after performing certain operations.

The most common use case for this unit test style is when you expect a method to modify some internal properties within your class or return a different result based on the input parameters. Another example will be if you’re expecting an exception during execution which will help test error handling logic within your codebase.

You perform state-based testing every time you test the result of the method. The example would be:

[Fact]
public void AddTwoNumbers_returns_sum_of_numbers()
{
    //Arrange
    var calculator = new Calculator();
    //Act
    var result = calculator.AddTwoNumbers(5, 6);
    //Assert
    Assert.Equal(11, result);
}

In the above example, the test method calls the AddTwoNumbers and checks the result of the method call.

What’s important here is knowing what specific parts of your codebase should be tested. For example, if you’re not too familiar with the codebase, it’s best to start by writing tests for methods with a return type other than void.

What is interaction-based testing?

Interaction-based unit testing is software testing that checks how different objects interact with each other. This type of testing can help verify the functionality of code that depends on the interaction between multiple classes or interfaces.

One example will be if you have a class that calls external service, e.g., performs a network call. In this case, you might want to write an interaction-based test to verify that the class has sent the network call. The other use case is for testing the void methods.

The interaction testing uses a mock object to check that the expected behavior happened.

Let’s say we want to test the following LoginUser method.

public async Task LoginUser()
{
    //get user by email
    var user = (await _userRepository.GetAllAsync())
        .FirstOrDefault(x => x.Email == Email.Value);
    if (user == null)
    {
        await _dialogMessage.DisplayAlert("Error",
                                            "Credentials are wrong.",
                                            "Ok");
        return;
    }
    //check that the hashed passwords match
    if (!SecurePasswordHasher.Verify(Password.Value, user.HashedPassword))
    {
        await _dialogMessage.DisplayAlert("Error",
                                          "Credentials are wrong.",
                                          "Ok");
        return;
    }
    //navigate to the main flow
    _navigationService.GoToMainFlow();
}

The test for the above method looks like this:

[Fact]
public async void LoginCommand_shows_error_when_email_is_not_correct()
{
    var mockDialogMessage = new Mock<IDialogMessage>();
    var stubRepository = new Mock<IRepository<User>>();
    stubRepository
        .Setup(x => x.GetAllAsync())
        .ReturnsAsync(new List<User> { });
 
    LoginViewModel viewModel = new LoginViewModel(null,
                                                    stubRepository.Object,
                                                    mockDialogMessage.Object,
                                                    _mockUserPreferences.Object);
    viewModel.Email.Value = "email@crypto.com";
    viewModel.Password.Value = "pass";
 
    await viewModel.LoginUser();
 
    mockDialogMessage
        .Verify(x => x.DisplayAlert(It.IsAny<string>(),
                                    "Credentials are wrong.",
                                    It.IsAny<string>()), Times.Once);
}

The above test case uses the Moq mocking framework. The test starts by setting up mockDialogMessage and stubRepository. After it calls the LoginUser method, it checks that the DisplayAlert was called using the Verify method of the mockDialogMessage mock object.

What is essential here, though, is knowing which methods in the class should be tested with unit tests and which with other types of testing. The danger of interaction-based tests is that they often check the implementation detail rather than observable behavior. Implementation detail is a code detail that is not externally visible and is only helpful for the developer working on the code.

The other danger with interaction-based testing is that it leads to over-testing. Over-testing is when unit tests are written for behavior that should be tested by integration tests or end-to-end tests.

State vs interaction-based testing: What’s the difference?

State-based tests provide a clear understanding of the system’s state at any given moment in time. As a result, they’re more straightforward to follow than interaction-based tests. This makes them easier to debug and ensures that nothing is missed.

However, state-based tests cannot show how an application will behave when it interacts with other apps or systems, which means they might not detect bugs that would arise in the real world. On the flip side, interaction-based tests can produce more detailed data about how an app will interact with other apps and systems in the real world because they simulate these interactions. But because they take more time to.

Pros and Cons of Each Type of Test

State-based and interaction based testing have their advantages and disadvantages.

Advantages of state-based testing

Advantages of state based testing are:

  • Easy to understand when learning
  • Easy to write
  • Easy to maintain
  • Easy to read the test code
  • Easy to test against different input parameters
  • Don’t require too much mocking
  • Provide good support for refactoring

Disadvantages of state-based testing

Disadvantages of state-based testing are:

  • It does not test the interaction between objects
  • It is not as accurate as a true functional test
  • Can’t be used on void methods

Advantages of interaction-based testing

Advantages of interaction-based testing are:

  • Tests the communication between objects
  • Covers interaction with external services
  • Covers complex methods

Disadvantages of interaction-based testing

Disadvantages of interaction-based testing are:

  • Hard to write
  • Hard to understand
  • Lead to more fragile tests
  • Provide less space for refactoring
  • Not 100% reliable

When Should You Use Which Type of Test?

State-based tests are best suited for testing classes that don’t depend on any external resources. However, if your class depends on an external resource, interaction-based tests are better.

In general, state-based testing is preferred because it requires less preparation and setup time than the interaction-based approach, making them faster to write and maintain. But on the other hand, they can miss some of the things that might happen when an app interacts with its environment.

On the flip side, interaction-based tests require more work from your end. You’ll have to mock all external resources used by your class before running any test cases but produce much richer data about how different parts of your application communicate with the external services. In addition to this, they can help you with some integration bugs that might not be found using state-based testing.

Conclusion

The two types of testing are different in the way they work.

State-based tests provide a clear understanding of the system’s state at any given moment in time but can miss some bugs that might happen when an app interacts with its environment. On the flip side, interaction-based tests can help catch bugs that occur in situations where the state of an app is not clearly defined.

You need to understand their limitations and strengths so you don’t overemphasize either type of testing in their development process. Ultimately, using a mix of state-based and interaction-based tests will help ensure the most comprehensive test coverage for your app.

Recent Posts