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.