Blazor Tuesday Tip: Test User Behavior Not Implementation Details With bUnit

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:


@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++;
}
}

view raw

Counter.razor

hosted with ❤ by GitHub

 

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):


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);
}
}

view raw

CounterTests.cs

hosted with ❤ by GitHub

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 Countercomponent 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.


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");
}
}

view raw

CounterTests.cs

hosted with ❤ by GitHub

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.