As Blazor starts to gain popularity, I see some developers making a common mistake about what they should be testing with bUnit, a testing library for Blazor.
Tip: Test user behavior, not implementation details
To quote Kent C Dodds, author of react-testing-library (a library similar to bUnit but for React):
The more your tests resemble the way your software is used, the more confidence they can give you.
This mantra of course applies to any testing that we do, whether client-side or server-side. The more you rely on mocks or start testing implementation details, the less confidence you will have that your software behaves how you expect it to. Moreover, it can make refactoring hard in the future if your tests are tightly coupled with implementation details, because any change in code results in broken tests.
What’s a practical application of this with Blazor?
Let’s say we have a Counter
component that I’m sure you’ve seen a million times. It’s a simple component that simply increments the count by 1 every time a button is clicked:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@page "/counter" | |
<h1>Counter</h1> | |
<p id="current-count">Current count: @CurrentCount</p> | |
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> | |
@code { | |
public int CurrentCount { get; set; } | |
private void IncrementCount() | |
{ | |
CurrentCount++; | |
} | |
} |
So we want a test for when the button is clicked, the count is incremented, right? If you take that test criteria literally, then you might write something like this (note: we’d likely want another test to assert that the initial value is 0):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CounterTests : TestContext | |
{ | |
[Fact] | |
public void ShouldIncrementCountWhenButtonIsClicked() | |
{ | |
var counterComponent = RenderComponent<Counter>(); | |
counterComponent.Find("button").Click(); | |
// This assertion tests that the state has incremented by 1 | |
counterComponent.Instance.CurrentCount.Should().Be(1); | |
} | |
} |
Run it and it works! We’re done, right? Wrong. This test is testing the implementation detail that CurrentCount
is stored in a property of the Counter
component which is irrelevant to our expected user behavior.
What’s wrong with this assertion?
This test gives us little confidence that the component actually does what we want it to do. We want to be sure that the CurrentCount
is displayed on the screen and increments when the button is clicked. That assertion does not guarantee CurrentCount
is even displayed on the page.
Moreover, this test is brittle. This test breaks if we change our implementation of where CurrentCount
is stored (such as a local variable, injected from DI, local storage, etc.), because our test is tied to our implementation of CurrentCount
being a property.
What’s the solution?
Focus on the user behavior. The behavior we expect is we want to make sure the count increments on the screen, so let’s query the component itself and evaluate the value.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CounterTests : TestContext | |
{ | |
[Fact] | |
public void ShouldIncrementCountWhenButtonIsClicked() | |
{ | |
var counterComponent = RenderComponent<Counter>(); | |
counterComponent.Find("button").Click(); | |
// Asserts actual user behavior | |
counterComponent.Find("#current-count").TextContent.Should().Be("Current count: 1"); | |
} | |
} |
Now our test assures us gives us more confidence, because we are asserting we are displaying the count on the screen AND it’s less brittle for future changes. It doesn’t matter if we pivot to use some Redux-style state management library or just use a local variable, this test will pass either way.
Hope this helps! For more reading on avoiding testing implementation details, you can read Kent C. Dodds’ post on Testing Implementation Details.
[…] Blazor Tuesday Tip: Test User Behavior Not Implementation Details With bUnit – Scott Sauber […]