Using test-driven development with a focus on accessibility improves the quality of Blazor components.
Blazor’s component model is one of the framework’s greatest strengths. Creating components in the framework feels intuitive and creative—most of the time things “just work.” Combine the component architecture with a rich set of tools for testing and you’ll find Blazor offers a productive developer experience that feels unmatched by anything before it. While that last bit may sound sensationalized, my latest experiment makes it feel justified.
This post was written and published as part of the 2021 C# Advent.
In an effort to learn more about accessibility on the web, I decided to experiment with an idea. I set out to build a component using test-driven development (TDD), but this time with an emphasis on accessibility. While I don’t feel this is a particularly unique idea, I was hard-pressed to find resources or guidance on the subject. However, I was able to find some generic examples of best practices to guide the way.
When the experiment was finished, I had a completed example that was thoroughly tested to specification. The best part was how much I learned about accessibility, keyboard navigation and unit testing components. In this article I’ll describe the key concepts and findings from the process and how to apply them in your next application.
Blazor has a quite extensive testing ecosystem which includes: bUnit, xUnit, Visual Studio and others. Since Blazor is written using .NET, some of these tools have evolved over the life of the platform, while others like bUnit were newly created for testing Blazor components exclusively.
bUnit is a testing library for Blazor Components. Its goal is to make it easy to write comprehensive, stable unit tests.
Directly quoting the bUnit website, “bUnit is a testing library for Blazor Components. Its goal is to make it easy to write comprehensive, stable unit tests.” bUnit is developed by .NET community member Egil Hansen, and the project is the de facto standard of component unit testing for Blazor.
bUnit works along with popular test frameworks such as: xUnit, NUnit and MSTest. These frameworks set up the test environment, while bUnit focuses on rendering and navigating components and their rendered markup.
In this experiment we’ll be using xUnit and bUnit, with the addition of a third helper library called FluentAssertions. FluentAssertions isn’t necessary here, but it greatly enhances readability of the test code by providing an easy-to-follow syntax when writing assertions.
Installation, setup instructions and documentation are easy find and follow on the bUnit website. Once the test project is created and configured, bUnit tests are written as Razor components. This C# in markup experience brings an ease of use factor to component testing by nearly eliminating the need to escape from HTML while composing tests.
Let’s take a look at a simple test scenario in the code below. Tests can be executed with Visual Studio or the CLI dotnet test
command. We’ll take a simple component that renders an h3
tag with text, and we’ll call this component under test Example.razor
.
Example.razor
<h3>Example</h3>
To test the component, we’ll create a test fixture called Example_Tests.razor
. It too will use the .razor
extension so it can utilize the razor syntax. We can optionally inherit from TestContext
, which gives us quick access to test APIs.
Next, we’ll use xUnit to define a Fact
, a type of test method. Then the component under test (cut
) is rendered by bUnit using the Render method. Once rendered, we can assert the component’s markup was rendered correctly by calling MarkupMatches
.
Example_Tests.razor
@inherits TestContext
@code {
[Fact]
public void ShouldRunTests()
{
var ctx = Render(@<Example/>);
ctx.MarkupMatches(@<h3>Example</h3>);
}
}
This is the basic premise of testing with bUnit. Later on we’ll be using more advanced techniques to assert specific HTML attribute values and trigger component events. For example, we can use bUnit and FluentAssertions to validate a button’s id
using the statement below.
// Does this div have the expected id?
cut.Find("div").Id.Should().Be(value);
Now that we’re ready to write tests, we’ll need to define our component’s specification.
Creating a specification for a component with a focus on accessibility is the most important part of the process. The spec is going to dictate everything that will happen next, so it should be as detailed as possible.
The real challenge here is finding expert guidance if you (like me) are not an accessibility expert. Using semantic elements and ARIA attributes incorrectly can actually do more harm than good, so it’s important to be aware of the nuances of assistive technologies and how they interpret HTML tags and attributes.
A good place to start if you’re learning about accessibility is WAI-ARIA Authoring Practices by the W3C Working Group. In this document you’ll find the specs for an accordion component, which are used in this experiment. The specs outline a detailed plan for implementing proper role, property, state and tabindex attributes, as well as keyboard support. The following tables are included on the Authoring Practices site.
Role or Attribute | Element | Usage |
---|---|---|
h3 |
|
|
aria-expanded="true" |
button |
Set to true when the Accordion panel is expanded, otherwise set to false .
|
aria-controls="ID" |
button |
Points to the ID of the panel which the header controls. |
aria-disabled="true" |
button |
If the accordion panel is expanded and is not allowed to be collapsed, then set to true .
|
region |
div |
Creates a landmark region that contains the currently expanded accordion panel. |
aria-labelledby="IDREF" |
div |
|
Key | Function |
---|---|
Space or Enter | When focus is on the accordion header of a collapsed section, expands the section. |
Tab |
|
Shift + Tab |
|
Down Arrow |
|
Up Arrow |
|
Home | When focus is on an accordion header, moves focus to the first accordion header. |
End | When focus is on an accordion header, moves focus to the last accordion header. |
To get an idea of what is being built and tested in this experiment, see the interactive Blazor REPL embedded below. This example represents the completed component to spec written in Blazor.
With the specification outlined, the next step is to create a test file. I personally like to use the convention ComponentName_Tests.razor
, but feel free to use a practice you like. For this example, we’ll use AriaAccordion_Tests.razor
. Inside the new test, we’ll set up a test fixture then copy the spec right inside as comments.
AriaAccordion_Tests.razor
@inherits TestContext
@code {
// Attribute Tests
// Element that serves as an accordion header.
// Each accordion header element contains a button that controls the visibility of its content panel.
// The example uses heading level 3 so it fits correctly within the outline of the page; the example is contained in a section titled with a level 2 heading.
// aria-expanded="true" button
// Set to true when the Accordion panel is expanded, otherwise set to false.
... specs continued
// End
// When focus is on an accordion header, moves focus to the last accordion header.
}
As with any TDD process, there’s a bit of a chicken-and-egg moment where tests need to be defined, yet there’s nothing to test. Depending on how test-driven your process is, you may want to write some tests before any components are defined.
Currently we have an empty test fixture with 13 accessibility specs. These specs alone aren’t enough to get started—we’ll also need some basic markup to render. At this point, we have enough information to write an expected markup test before moving on to more targeted tests. It’s a bit of a balancing act as we abstract the component and its subcomponents away to reach some basic functionality.
It’s a bit of a balancing act as we abstract the component and its subcomponents away to reach some basic functionality.
Let’s begin with the basic HTML structure provided in the spec. Using the HTML, we’ll create two components—AriaAccordion
and AccordionPanel
. The components are added to the project to serve as basic building blocks. Their usage is outlined in the snippet below.
Component Usage
<AriaAccordion>
<AccordionPanel Title="My Title A">
<p>My Content for panel A</p>
</AccordionPanel>
<AccordionPanel Title="My Title B">
<p>My Content for panel B</p>
</AccordionPanel>
</AriaAccordion>
See a fully interactive example below using Telerik REPL for Blazor.
Our first unit test will validate the initial render state of the component. For this we’ll write a test that uses Render and MarkupMatches.
[Fact]
public void FirstRenderMarkupCorrect()
{
var cut = Render(@<AriaAccordion>
<AccordionPanel Title="P1">
<p>P1 Content</p>
</AccordionPanel>
<AccordionPanel Title="P2">
<p>P2 Content</p>
</AccordionPanel>
</AriaAccordion>);
cut.MarkupMatches(
@<div class="Accordion">
<h3>
<button class="Accordion-trigger">
<span class="Accordion-title">P1
<span class="Accordion-icon"></span>
</span>
</button>
</h3>
<div class="Accordion-panel">
<div>
<p>P1 Content</p>
</div>
</div>
... second panel's html
</div>);
Now that we have a working test and components, we can begin adding features.
When writing our tests, we’ll work on items in an order that makes sense logically. In the case of our accordion, we need functionality to expand and collapse panels before other features can be considered. Let’s begin with the spec for aria-expanded
since it correlates directly with the feature we need to implement first.
For this spec, we’ll first update our FirstRenderMarkupCorrect
test to include the default state for two panels when they are first rendered. This will ensure a new accordion renders in the expected default state before changes are applied. The MarkupMatches assertion is updated so the first button has the attribute aria-expanded="true"
, and the second panel has the attribute hidden
.
cut.MarkupMatches(@<div class="Accordion">
... first panel's html
<button class="Accordion-trigger" aria-expanded="true">
... second panel's html
<div class="Accordion-panel" hidden> ...);
With the default state covered, we can then focus on the feature. In the test below we’ll use a convenience method to render two panels to a cut
. Next, we’ll find both buttons in the accordion, then use the Click
method to simulate a button click on the UI. Last, the two buttons are used and their aria-expanded
attributes are retrieved. The attributes’ values are asserted with Should().Be("value")
.
// aria-expanded="true/false" button
[Fact(DisplayName =
"Set to true when the Accordion panel is expanded, otherwise set to false.")]
public void AriaExpanedAttribute()
{
var cut = RenderAccordionWithTwoPanels();
var button1 = cut.Find("h3:nth-of-type(1) > button");
var button2 = cut.Find("h3:nth-of-type(2) > button");
button2.Click();
button1.GetAttribute("aria-expanded").Should().Be("false");
button2.GetAttribute("aria-expanded").Should().Be("true");
}
A screen reader will assume the element has no expanded state if the attribute is missing.
With these tests in place we can begin implementing the feature in the component. In the case of the aria-expanded
attribute, I made an interesting observation. By convention, Blazor will omit attributes when their value is false
. This is helpful for most attributes that are implicitly “truthy.” For example, the hidden
and disabled
attributes when rendered imply true
, and false
when they do not exist. However, with aria-expanded
this is not the case—a screen reader will assume the element has no expanded state if the attribute is missing.
To resolve this issue, we must instruct Blazor to always render a value. Consider the following example where the first statement produces no attribute, while the second produces aria-expanded="false"
.
aria-expanded="@IsExpanded"
aria-expanded="@IsExpanded.ToString().ToLowerInvariant()"
IsExpanded = false;
While hidden
isn’t a line item included in the spec, it is implied by the HTML/CSS provided. To validate the feature, we’ll add a test similar to aria-expand
. In this test we’ll get the AccordionPanel components. Next, we’ll find the second button and invoke a click. After the button is clicked, the hidden
attribute should be rendered only on the second panel’s div element. We can assert this using HasAttribute
with the fluent assertion Should().BeTrue/BeFalse()
.
[Fact(DisplayName =
"Set hidden to true when the Accordion panel is collapsed, otherwise set to false.")]
public void ClickingExpandShouldToggleHiddenAttribute()
{
var cut = RenderAccordionWithTwoPanels();
var panels = cut.FindComponents<AccordionPanel>();
panels[1].Find("button").Click();
panels[0].Find("div").HasAttribute("hidden").Should().BeTrue();
panels[1].Find("div").HasAttribute("hidden").Should().BeFalse();
}
A fair amount of new component code is required to pass the tests. Once tests are passing, the component begins to perform some basic operations. An interactive sample can be seen below.
Ed