bUnit bUnit
Search Results for

    Show / Hide Table of Contents

    Writing tests for Blazor components

    Testing Blazor components is a little different from testing regular C# classes: Blazor components are rendered, they have the Blazor component life cycle during which we can provide input to them, and they can produce output.

    Use bUnit to render the component under test, pass in its parameters, inject required services, and access the rendered component instance and the markup it has produced.

    Rendering a component happens through bUnit's TestContext. The result of the rendering is an IRenderedComponent, referred to as a "rendered component", that provides access to the component instance and the markup produced by the component.

    Note

    The preview and beta versions of bUnit included an experimental feature for writing tests using two test components, <Fixture> and <SnapshotTest>. Since there is now a better way to write tests in .razor files, the old experimental feature has been moved into a separate project named bunit.web.testcomponents. It is kept around to not break early adopters, but no additional features or improvements are planned to it.

    To learn more, head over to the bunit.web.testcomponents project on GitHub.

    Write tests in .cs or .razor files

    bUnit works with MSTest, NUnit, and xUnit, and it allows you to write the unit tests in either .cs or .razor files.

    The latter, writing tests in .razor files, provides an easier way to declare component markup and HTML markup in the tests, so it will most likely be the go-to for many people in the future.

    However, the current Razor editor in Visual Studio 2019 does not offer all the code editing features available in the C# editor, and has some formatting bugs on top of that, so that is something to consider if you choose to write tests in .razor files.

    The following sections show how to get started writing tests in either .cs or .razor files.

    Creating basic tests in .razor files

    Before writing tests in .razor files, a few things needs to be in place:

    1. Make sure the test project has the SDK type set to Microsoft.NET.Sdk.Razor. Otherwise the Blazor compiler will not translate your .razor files into runnable code.
    2. Add an _Imports.razor file to the test project. It serves the same purpose as _Imports.razor files in regular Blazor projects. These using statements are useful to add right away:
    @using Microsoft.AspNetCore.Components.Forms
    @using Microsoft.AspNetCore.Components.Web
    @using Microsoft.JSInterop
    @using Microsoft.Extensions.DependencyInjection
    @using AngleSharp.Dom
    @using Bunit
    @using Bunit.TestDoubles
    

    Also add an using statement for your general purpose testing framework, e.g. @using Xunit for xUnit.

    With that in place, lets look at a simple example that tests the following <HelloWorld> component:

    <h1>Hello world from Blazor</h1>
    
    • xUnit
    • NUnit
    • MSTest
    @code
    {
      [Fact]
      public void HelloWorldComponentRendersCorrectly()
      {
        // Arrange
        using var ctx = new TestContext();
    
        // Act
        var cut = ctx.Render(@<HelloWorld />);
    
        // Assert
        cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
      }
    }
    
    @code
    {
      [Test]
      public void HelloWorldComponentRendersCorrectly()
      {
        // Arrange
        using var ctx = new Bunit.TestContext();
    
        // Act
        var cut = ctx.Render(@<HelloWorld />);
    
        // Assert
        cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
      }
    }
    
    Note

    TestContext is an ambiguous reference - it could mean Bunit.TestContext or NUnit.Framework.TestContext - so you have to specify the Bunit namespace when referencing TestContext to resolve the ambiguity for the compiler.

    @attribute [TestClass]
    @code 
    {
      [TestMethod]
      public void HelloWorldComponentRendersCorrectly()
      {
        // Arrange
        using var ctx = new Bunit.TestContext();
    
        // Act
        var cut = ctx.Render(@<HelloWorld/>);
    
        // Assert
        cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
      }
    }
    
    Note

    TestContext is an ambiguous reference - it could mean Bunit.TestContext or Microsoft.VisualStudio.TestTools.UnitTesting.TestContext - so you have to specify the Bunit namespace when referencing TestContext to resolve the ambiguity for the compiler.

    The test above does the following:

    1. Creates a new instance of the disposable bUnit TestContext, and assigns it to ctx the variable using the using var syntax to avoid unnecessary source code indention.
    2. Renders the <HelloWorld> component using TestContext, which is done through the Render(RenderFragment) method. We cover passing parameters to components on the Passing parameters to components page.
    3. Verifies the rendered markup from the <HelloWorld> component using the MarkupMatches method. The MarkupMatches method performs a semantic comparison of the expected markup with the rendered markup.
    Tip

    Learn more about how the semantic HTML/markup comparison in bUnit works, and how to customize it, on the Customizing the semantic HTML comparison page.

    Tip

    In bUnit tests, we like to use the abbreviation CUT, short for "component under test", to indicate the component that is being tested. This is inspired by the common testing abbreviation SUT, short for "system under test".

    Secret sauce of '.razor' files tests

    The trick employed in these tests is the "inline Razor templates syntax", i.e. where a render fragment is simply created using the @<{HTML tag}>...</{HTML tag}> notation. In that notation there is no need to do any escaping of e.g. the quotation mark ("), that is usually associated with working with markup in C# code.

    One small caveat to be aware of is that the inline Razor templates syntax only supports one outer element, e.g. this is OK:

    @<Foo>
       <Bar>
         <Baz>...</Baz>
       </Bar>
     </Foo>
    

    However, this will not work:

    @<Foo></Foo>
     <Bar></Bar>
    

    There is a simple workaround though: wrap all elements in the special Blazor element <text>. The <text> element will not be part of the rendered output, but it provides a simple way to group multiple root elements into a single inline Razor template. E.g.:

    @<text>
       <Foo></Foo>
       <Bar></Bar>
     </text>
    

    Remove boilerplate code from tests

    We can remove some boilerplate code from each test by making the TestContext implicitly available to the test class, so we don't have to have using var ctx = new Bunit.TestContext(); in every test. This can be done like so:

    • xUnit
    • NUnit
    • MSTest
    @inherits TestContext
    @code
    {
      [Fact]
      public void HelloWorldComponentRendersCorrectly()
      {
        // Act
        var cut = Render(@<HelloWorld />);
    
        // Assert
        cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
      }
    }
    

    Since xUnit instantiates test classes for each execution of the test methods inside them, and disposes of them after each test method has run, we simply inherit from TestContext, and methods like Render(RenderFragment) can then be called directly from each test. This is seen in the listing above.

    @inherits BunitTestContext
    @code
    {
      [Test]
      public void HelloWorldComponentRendersCorrectly()
      {
        // Act
        var cut = Render(@<HelloWorld />);
    
        // Assert
        cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
      }
    }
    
    using Bunit;
    using NUnit.Framework;
    
    namespace Bunit.Docs.Samples
    {
      public abstract class BunitTestContext : TestContextWrapper
        {
        [SetUp]
        public void Setup() => TestContext = new Bunit.TestContext();
    
        [TearDown]
        public void TearDown() => TestContext?.Dispose();
      }
    }
    

    Since NUnit instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext, listed above, and use that to hook into NUnit's [SetUp] and [TearDown] methods, which runs before and after each test. The BunitTestContext class inherits from the TestContextWrapper type, which is included specifically to make this scenario easier.

    Then methods like Render(RenderFragment) can be called directly from each test, as seen in the listing above.

    @attribute [TestClass]
    @inherits BunitTestContext
    @code
    {
      [TestMethod]
      public void HelloWorldComponentRendersCorrectly()
      {
        // Act
        var cut = Render(@<HelloWorld/>);
    
        // Assert
        cut.MarkupMatches(@<h1>Hello world from Blazor</h1>);
      }
    }
    
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Bunit.Docs.Samples
    {
        public abstract class BunitTestContext : TestContextWrapper
        {
            [TestInitialize]
            public void Setup() => TestContext = new Bunit.TestContext();
    
            [TestCleanup]
            public void TearDown() => TestContext?.Dispose();
        }
    }
    

    Since MSTest instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext, listed above, and use that to hook into MSTest's [TestInitialize] and [TestCleanup] methods. This runs before and after each test. The BunitTestContext class inherits from the TestContextWrapper type, which is included specifically to make this scenario easier.

    Then methods like Render(RenderFragment) can be called directly from each test, as seen in the listing above.

    Important

    All the examples in the documentation explicitly new up a TestContext, i.e. using var ctx = new TestContext(). If you are using the trick above and have your test class inherit from TestContext, you should NOT new up a TestContext in test methods also.

    Simply call the test context's methods directly, as they are available in your test class.

    For example, var cut = ctx.Render(@<HelloWorld/>);
    becomes var cut = Render(@<HelloWorld/>);.

    Creating basic tests in .cs files

    This is a simple example of writing tests in .cs files which tests the following <HelloWorld> component:

    <h1>Hello world from Blazor</h1>
    
    • xUnit
    • NUnit
    • MSTest
    using Xunit;
    using Bunit;
    
    namespace Bunit.Docs.Samples
    {
      public class HelloWorldTest
      {
        [Fact]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Arrange
          using var ctx = new TestContext();
    
          // Act
          var cut = ctx.RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    
    using Bunit;
    using NUnit.Framework;
    
    namespace Bunit.Docs.Samples
    {
      public class HelloWorldTest
      {
        [Test]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Arrange
          using var ctx = new Bunit.TestContext();
    
          // Act
          var cut = ctx.RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    
    Note

    TestContext is an ambiguous reference - it could mean Bunit.TestContext or NUnit.Framework.TestContext - so you have to specify the Bunit namespace when referencing TestContext to resolve the ambiguity for the compiler. Alternatively, you can give bUnit's TestContext a different name during import, e.g.: using BunitTestContext = Bunit.TestContext;

    using Bunit;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Bunit.Docs.Samples
    {
      [TestClass]
      public class HelloWorldTest
      {
        [TestMethod]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Arrange
          using var ctx = new Bunit.TestContext();
    
          // Act
          var cut = ctx.RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    
    Note

    TestContext is an ambiguous reference - it could mean Bunit.TestContext or Microsoft.VisualStudio.TestTools.UnitTesting.TestContext - so you have to specify the Bunit namespace when referencing TestContext to resolve the ambiguity for the compiler. Alternatively, you can give bUnit's TestContext a different name during import, e.g.:
    using BunitTestContext = Bunit.TestContext;

    The test above does the following:

    1. Creates a new instance of the disposable bUnit TestContext, and assigns it to the ctx variable using the using var syntax to avoid unnecessary source code indention.
    2. Renders the <HelloWorld> component using TestContext, which is done through the RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>) method. We cover passing parameters to components on the Passing parameters to components page.
    3. Verifies the rendered markup from the <HelloWorld> component using the MarkupMatches method. The MarkupMatches method performs a semantic comparison of the expected markup with the rendered markup.
    Tip

    Learn more about how the semantic HTML/markup comparison in bUnit works, and how to customize it, on the Customizing the semantic HTML comparison page.

    Tip

    In bUnit tests, we like to use the abbreviation CUT, short for "component under test", to indicate the component that is being tested. This is inspired by the common testing abbreviation SUT, short for "system under test".

    Remove boilerplate code from tests

    We can remove some boilerplate code from each test by making the TestContext implicitly available to the test class, so we don't have to have using var ctx = new Bunit.TestContext(); in every test. This can be done like so:

    • xUnit
    • NUnit
    • MSTest
    using Xunit;
    using Bunit;
    
    namespace Bunit.Docs.Samples
    {
      public class HelloWorldImplicitContextTest : TestContext
      {
        [Fact]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Act
          var cut = RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    

    Since xUnit instantiates test classes for each execution of the test methods inside them, and disposes of them after each test method has run, we simply inherit from TestContext, and methods like RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>) can then be called directly from each test. This is seen in the listing above.

    using Bunit;
    using NUnit.Framework;
    
    namespace Bunit.Docs.Samples
    {
      public class HelloHelloWorldImplicitContextTest : BunitTestContext
      {
        [Test]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Act
          var cut = RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    
    using Bunit;
    using NUnit.Framework;
    
    namespace Bunit.Docs.Samples
    {
      public abstract class BunitTestContext : TestContextWrapper
        {
        [SetUp]
        public void Setup() => TestContext = new Bunit.TestContext();
    
        [TearDown]
        public void TearDown() => TestContext?.Dispose();
      }
    }
    

    Since NUnit instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext, listed above, and use that to hook into NUnit's [SetUp] and [TearDown] methods, which run before and after each test.

    Then methods like RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>) can be called directly from each test, as seen in the listing above.

    using Bunit;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Bunit.Docs.Samples
    {
      [TestClass]
      public class HelloHelloWorldImplicitContextTest : BunitTestContext
      {
        [TestMethod]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Act
          var cut = RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Bunit.Docs.Samples
    {
        public abstract class BunitTestContext : TestContextWrapper
        {
            [TestInitialize]
            public void Setup() => TestContext = new Bunit.TestContext();
    
            [TestCleanup]
            public void TearDown() => TestContext?.Dispose();
        }
    }
    

    Since MSTest instantiates a test class only once for all tests inside it, we cannot simply inherit directly from TestContext as we want a fresh instance of TestContext for each test. Instead, we create a helper class, BunitTestContext, listed above, and use that to hook into MSTest's [TestInitialize] and [TestCleanup] methods. This runs before and after each test.

    Then methods like RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>) can be called directly from each test, as seen in the listing above.

    Important

    All the examples in the documentation explicitly new up a TestContext, i.e. using var ctx = new TestContext(). If you are using the trick above and have your test class inherit from TestContext, you should NOT new up another TestContext in test methods also.

    Simply call the test contest's methods directly, as they are available in your test class.

    For example, var cut = ctx.RenderComponent<HelloWorld>();
    becomes var cut = RenderComponent<HelloWorld>();.

    Further reading

    With the basics out of the way, next we will look at how to pass parameters and inject services into our component under test. After that, we will cover the ways in which we can verify the outcome of a rendering in more detail

    • Passing parameters to components
    • Injecting services into components under test
    • Verifying markup from a component
    • Verifying the state of a component under test
    • Triggering event handlers in components

    Premium sponsor: Progress Telerik.

    Editorial support provided by Packt.

    Supported by the .NET Foundation.

    • Improve this Doc
    Back to top Documentation updated on 03/10/2023 10:42:43 +00:00 in commit d48e4498d5.