Telerik blogs
BlazorT4_1200x303

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.

Tools Used

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.

Component 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, Property, State and Tabindex Attributes

Role or Attribute Element   Usage
h3
  • 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.
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
  • Defines the accessible name for the region element.
  • References the accordion header button that expands and collapses the region.
  • region elements are required to have an accessible name to be identified as a landmark.

Keyboard Support

Key Function
Space or Enter When focus is on the accordion header of a collapsed section, expands the section.
Tab
  • Moves focus to the next focusable element.
  • All focusable elements in the accordion are included in the page Tab sequence.
Shift + Tab
  • Moves focus to the previous focusable element.
  • All focusable elements in the accordion are included in the page Tab sequence.
Down Arrow
  • When focus is on an accordion header, moves focus to the next accordion header.
  • When focus is on last accordion header, moves focus to first accordion header.
Up Arrow
  • When focus is on an accordion header, moves focus to the previous accordion header.
  • When focus is on first accordion header, moves focus to last accordion header.
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.

Our Experiment

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

Rendering Tests

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.

Attribute Tests

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.

Keyboard Navigation Tests

This section of testing will ensure the accordion component is easily navigated with a keyboard. One of the most important aspects of web accessibility is keyboard accessibility. Users with motor needs rely on a keyboard. This includes people with limited use of their hands and users with modified keyboards or other hardware that mimics the functionality of a keyboard. Low-vision users also often use a keyboard for navigation.

The first spec is a point of interest. The spec defines interaction for a Space or Enter key press when an accordion header has focus. In the browser, the default behavior of buttons and some inputs is to trigger an onclick event. Because this is the inherent behavior and we haven’t added code to handle these keys specifically, they will perform their default action.

In addition, bUnit is operating with rendered markup, and default browser behaviors like this aren’t unit-testable. Instead of writing a unit test, we should defer this test to browser-based functional UI testing with a tool like Telerik Test Studio.

Similar to Space and Enter, the Tab and Shift + Tab specs call out for default browser behavior. Defaults seem like something we can simply ignore—however, we should still defer to functional tests to cover these items. While we don’t intend to modify these behaviors, there is still a potential for our code or markup to unintentionally interrupt the default action.

One such example is when the tabindex attribute is used. Even correct usage of tabindex can cause side effects that impact UI navigation of neighboring components.

Even correct usage of tabindex can cause side effects that impact UI navigation of neighboring components.

The next set of keyboard specs are not default behavior and need further development to implement. The up arrow and down arrow keys are used to navigate accordion headers and perform a “looping” behavior when the top and bottom of panels are focused. Since we’ll be attaching an onkeydown event handler for these items, they can be tested with bUnit.

In addition, we’ll be setting focus to an element—but this comes with a caveat. Because bUnit is testing a rendered component, focus behaves differently. Instead of checking if an element is focused, we’ll need to check if the element was given focus. The JSInterop.VerifyFocusAsyncInvoke helper in bUnit will provide a mechanism for asserting if focus was given to an element reference.

// Down Arrow	
[Fact(DisplayName = 
    "When focus is on an accordion header, Down Arrow moves focus to the next accordion header.")]
public void DownArrowNavigation()
{
    var cut = RenderAccordionWithTwoPanels();
    var panels = cut.FindComponents<AccordionPanel>();
    var button1 = panels[0].Find("button");
    var button2 = panels[1].Find("button");

    button1.KeyDown(Key.Down);

    // Was Focus called, did it contain an argument for the correct element?
    JSInterop.VerifyFocusAsyncInvoke().Arguments[0].ShouldBeElementReferenceTo(button2);
}

Next we test the looping behavior when the last item is in focus.

[Fact(DisplayName = 
    "When focus is on last accordion header, Down Arrow moves focus to first accordion header.")]
public void DownArrowNavigationLoops()
{
    var cut = RenderAccordionWithTwoPanels();
    var panels = cut.FindComponents<AccordionPanel>();
    var button1 = panels[0].Find("button");
    var button2 = panels[1].Find("button");

    button2.KeyDown(Key.Down);
    
    // Was Focus called, did it contain an argument for the correct element?
    JSInterop.VerifyFocusAsyncInvoke().Arguments[0].ShouldBeElementReferenceTo(button1);
}

For brevity the up arrow tests are not shown since they’re nearly identical to the down arrow tests.

The last tests are for home and end keyboard navigation. These tests are also similar to the up/down arrow tests, however the setup is modified. To avoid any false positives, we’ll need more accordion panels to test. If we only use two items, it’s possible that our logic gives focus up or down by a single item instead of skipping directly to the first or last item. We’ll also invoke keydown on a middle element to ensure elements are skipped when the focus event is called.

// Home	
[Fact(DisplayName = 
    "When focus is on an accordion header, Home moves focus to the first accordion header.")]
public void HomeNavigation()
{
    var cut = RenderAccordionWithFourPanels();
    var panels = cut.FindComponents<AccordionPanel>();
    var button3 = panels[2].Find("button");
    var button1 = panels[0].Find("button");

    button3.KeyDown(Key.Home);
    
    // did button 3 skip focus to button 1?
    JSInterop.VerifyFocusAsyncInvoke().Arguments[0].ShouldBeElementReferenceTo(button1);
}

For brevity the end key tests are not shown since they’re nearly identical to the home key tests.

Using these tests, we can write the remaining component code. Once keyboard navigation is complete, the component is fully functional. Since we have full test coverage for our component, refactoring can be done without the worry of breaking functionality.

This example represents the completed component to spec, including keyboard navigation, written in Blazor.

Final Thoughts

Through this process we saw that a well-defined specification can greatly improve the test story for our components. By defining accessibility specs up front, we can test our components for many accessibility features using TDD. When tests incorporate an accessibility-first approach, we can be confident components will meet the standards set in the specifications. Unit testing provides an isolated test with rapid verification, which is performed without running the application.

The process outlined here does have some weaknesses. First, an expert should be involved in drafting the spec for HTML, ARIA and keyboard navigation features. Incorrect usage of the aforementioned can cause flaws in the UI. In addition, test coverage should include browser-based functional testing. Functional tests further enhance the test coverage by ensuring the browser’s behaviors are as expected.

One final observation that came from this process is how complex accessibility is. Components should be built with a deep knowledge of accessibility so applications can be used by all users. If an you are not an expert and your team doesn’t have access to one, then an alternative is to use off-the-shelf components like Telerik UI for Blazor that already incorporate keyboard and accessibility features.


Ed Charbeneau is a Developer Advocate for Telerik
About the Author

Ed Charbeneau

Ed Charbeneau is a web enthusiast, speaker, writer, design admirer, and Developer Advocate for Telerik. He has designed and developed web based applications for business, manufacturing, systems integration as well as customer facing websites. Ed enjoys geeking out to cool new tech, brainstorming about future technology, and admiring great design. Ed's latest projects can be found on GitHub.

Related Posts

Comments

Comments are disabled in preview mode.