Table of Contents

Passing parameters to components

bUnit comes with a number of ways to pass parameters to components under test:

  1. In tests written in .razor files, passing parameters is most easily done with inside an inline Razor template passed to the Render method, although the parameter passing option available in tests written in C# files is also available here.

  2. In tests written in .cs files, bUnit includes a strongly typed builder. There are two methods in bUnit that allow passing parameters in C#-based test code:

    • RenderComponent method on the test context, which is used to render a component initially.
    • SetParametersAndRender method on a rendered component, which is used to pass new parameters to an already rendered component.

In the following sub sections, we will show both .cs- and .razor-based test code; just click between them using the tabs.

Note

The examples below are written using xUnit, but the code is the same with NUnit and MSTest.

The example tests are inheriting from the TestContext as described in the Writing tests for Blazor components page. If your test class is not inheriting from the TestContext, then you should explicitly instantiate the TestContext in your tests. The recommended way is to inherit from the TestContext. An example below will demonstrate how to instantiate a TestContext per test method.

Regular parameters

A regular parameter is one that is declared using the [Parameter] attribute. The following subsections will cover both non-Blazor type parameters, e.g. int and List<string>, and the special Blazor types like EventCallback and RenderFragment.

Non-Blazor type parameters

Let's look at an example of passing parameters that takes types which are not special to Blazor, i.e.:

public class NonBlazorTypesParams : ComponentBase
{
  [Parameter]
  public int Numbers { get; set; }

  [Parameter]
  public List<string> Lines { get; set; }
}

This can be done like this:

public class NonBlazorTypesParamsTest : TestContext
{
  [Fact]
  public void Test()
  {
    var lines = new List<string> { "Hello", "World" };

    var cut = RenderComponent<NonBlazorTypesParams>(parameters => parameters
      .Add(p => p.Numbers, 42)
      .Add(p => p.Lines, lines)
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s Add method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type for the value. This makes the builder's methods strongly typed and refactor-safe.

EventCallback parameters

This example will pass parameters to the following two EventCallback parameters:

public class EventCallbackParams : ComponentBase
{
  [Parameter]
  public EventCallback<MouseEventArgs> OnClick { get; set; }

  [Parameter]
  public EventCallback OnSomething { get; set; }
}

This can be done like this:

public class EventCallbackParamsTest : TestContext
{
  [Fact]
  public void Test()
  {
    Action<MouseEventArgs> onClickHandler = _ => { };
    Action onSomethingHandler = () => { };

    var cut = RenderComponent<EventCallbackParams>(parameters => parameters
      .Add(p => p.OnClick, onClickHandler)
      .Add(p => p.OnSomething, onSomethingHandler)
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s Add method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type of callback method. This makes the builder's methods strongly typed and refactor-safe.

ChildContent parameters

The ChildContent parameter in Blazor is represented by a RenderFragment. In Blazor, this can be regular HTML markup, it can be Razor markup, e.g. other component declarations, or a mix of the two. If it is another component, then that component can also receive child content, and so forth.

The following subsections have different examples of child content being passed to the following component:

public class ChildContentParams : ComponentBase
{
  [Parameter]
  public RenderFragment ChildContent { get; set; }
}

Passing HTML to the ChildContent parameter

public class ChildContentParams1Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent("<h1>Hello World</h1>")
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s AddChildContent method to pass an HTML markup string as the input to the ChildContent parameter.

Passing a component without parameters to the ChildContent parameter

To pass a component, e.g. the classic <Counter> component, which does not take any parameters itself, to a ChildContent parameter, do the following:

public class ChildContentParams2Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent<Counter>()
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s AddChildContent<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the component under test's ChildContent parameter.

Passing a component with parameters to the ChildContent parameter

To pass a component with parameters to a component under test, e.g. the <Alert> component with the following parameters, do the following:

[Parameter] public string Heading { get; set; }
[Parameter] public AlertType Type { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
public class ChildContentParams3Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent<Alert>(alertParameters => alertParameters
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s AddChildContent<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the component under test. The AddChildContent<TChildComponent> method takes an optional ComponentParameterCollectionBuilder<TComponent> as input, which can be used to pass parameters to the TChildComponent component, which in this case is the <Alert> component.

Passing a mix of Razor and HTML to a ChildContent parameter

Some times you need to pass multiple different types of content to a ChildContent parameter, e.g. both some markup and a component. This can be done in the following way:

public class ChildContentParams4Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<ChildContentParams>(parameters => parameters
      .AddChildContent("<h1>Below you will find a most interesting alert!</h1>")
      .AddChildContent<Alert>(childParams => childParams
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

Passing a mix of markup and components to a ChildContent parameter is done by simply calling the ComponentParameterCollectionBuilder<TComponent>'s AddChildContent() methods as seen here.

RenderFragment parameters

A RenderFragment parameter is very similar to the special ChildContent parameter described in the previous section, since a ChildContent parameter is of type RenderFragment. The only difference is the name, which must be anything other than ChildContent.

In Blazor, a RenderFragment parameter can be regular HTML markup, it can be Razor markup, e.g. other component declarations, or it can be a mix of the two. If it is another component, then that component can also receive child content, and so forth.

The following subsections have different examples of content being passed to the following component's RenderFragment parameter:

public class RenderFragmentParams : ComponentBase
{
  [Parameter]
  public RenderFragment Content { get; set; }
}

Passing HTML to a RenderFragment parameter

public class RenderFragmentParams1Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add(p => p.Content, "<h1>Hello World</h1>")
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s Add method to pass an HTML markup string as the input to the RenderFragment parameter.

Passing a component without parameters to a RenderFragment parameter

To pass a component such as the classic <Counter> component, which does not take any parameters, to a RenderFragment parameter, do the following:

public class RenderFragmentParams2Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add<Counter>(p => p.Content)
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s Add<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the RenderFragment parameter.

Passing a component with parameters to a RenderFragment parameter

To pass a component with parameters to a RenderFragment parameter, e.g. the <Alert> component with the following parameters, do the following:

[Parameter] public string Heading { get; set; }
[Parameter] public AlertType Type { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
public class RenderFragmentParams3Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add<Alert>(p => p.Content, alertParameters => alertParameters
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

The example uses the ComponentParameterCollectionBuilder<TComponent>'s Add<TChildComponent> method, where TChildComponent is the (child) component that should be passed to the RenderFragment parameter. The Add<TChildComponent> method takes an optional ComponentParameterCollectionBuilder<TComponent> as input, which can be used to pass parameters to the TChildComponent component, which in this case is the <Alert> component.

Passing a mix of Razor and HTML to a RenderFragment parameter

Some times you need to pass multiple different types of content to a RenderFragment parameter, e.g. both markup and and a component. This can be done in the following way:

public class RenderFragmentParams4Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<RenderFragmentParams>(parameters => parameters
      .Add(p => p.Content, "<h1>Below you will find a most interesting alert!</h1>")
      .Add<Alert>(p => p.Content, childParams => childParams
        .Add(p => p.Heading, "Alert heading")
        .Add(p => p.Type, AlertType.Warning)
        .AddChildContent("<p>Hello World</p>")
      )
    );
  }
}

Passing a mix of markup and components to a RenderFragment parameter is simply done by calling the ComponentParameterCollectionBuilder<TComponent>'s Add() methods or using the ChildContent() factory methods in ComponentParameterFactory, as seen here.

Templates parameters

Template parameters are closely related to the RenderFragment parameters described in the previous section. The difference is that a template parameter is of type RenderFragment<TValue>. As with a regular RenderFragment, a RenderFragment<TValue> template parameter can consist of regular HTML markup, it can be Razor markup, e.g. other component declarations, or it can be a mix of the two. If it is another component, then that component can also receive child content, and so forth.

The following examples renders a template component which has a RenderFragment<TValue> template parameter:

@typeparam TItem

<div id="generic-list">
  @foreach (var item in Items)
  {
    @Template(item)
  }
</div>

@code 
{
  [Parameter]
  public IEnumerable<TItem> Items { get; set; }

  [Parameter]
  public RenderFragment<TItem> Template { get; set; }
}

Passing HTML-based templates

To pass a template into a RenderFragment<TValue> parameter that just consists of regular HTML markup, do the following:

public class TemplateParams1Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<TemplateParams<string>>(parameters => parameters
      .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" })
      .Add(p => p.Template, item => $"<span>{item}</span>")
    );
  }
}

The examples pass a HTML markup template into the component under test. This is done with the help of a Func<TValue, string> delegate which takes whatever the template value is as input, and returns a (markup) string. The delegate is automatically turned into a RenderFragment<TValue> type and passed to the template parameter.

The example uses the ComponentParameterCollectionBuilder<TComponent>'s Add method to first add the data to the Items parameter and then to a Func<TValue, string> delegate.

The delegate creates a simple markup string in the example.

Passing a component-based template

To pass a template into a RenderFragment<TValue> parameter, which is based on a component that receives the template value as input (in this case, the <Item> component listed below), do the following:

<span>@Value</span>
@code 
{
  [Parameter]
  public string Value { get; set; }
}
public class TemplateParams2Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<TemplateParams<string>>(parameters => parameters
      .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" })
      .Add<Item, string>(p => p.Template, value => itemParams => itemParams
        .Add(p => p.Value, value)
      )
    );
  }
}

The example creates a template with the <Item> component listed above.

Unmatched parameters

An unmatched parameter is a parameter that is passed to a component under test, and which does not have an explicit [Parameter] parameter but instead is captured by a [Parameter(CaptureUnmatchedValues = true)] parameter.

In the follow examples, we will pass an unmatched parameter to the following component:

public class UnmatchedParams : ComponentBase
{
  [Parameter(CaptureUnmatchedValues = true)]
  public Dictionary<string, object> InputAttributes { get; set; }
}
public class UnmatchedParamsTest : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<UnmatchedParams>(parameters => parameters
      .AddUnmatched("some-unknown-param", "a value")
    );
  }
}

The examples passes in the parameter some-unknown-param with the value a value to the component under test.

Cascading Parameters and Cascading Values

Cascading parameters are properties with the [CascadingParameter] attribute. There are two variants: named and unnamed cascading parameters. In Blazor, the <CascadingValue> component is used to provide values to cascading parameters, which we also do in tests written in .razor files. However, for tests written in .cs files we need to do it a little differently.

The following examples will pass cascading values to the <CascadingParams> component listed below:

@code 
{
  [CascadingParameter]
  public bool IsDarkTheme { get; set; }

  [CascadingParameter(Name = "LoggedInUser")]
  public string UserName { get; set; }
    
  [CascadingParameter(Name = "LoggedInEmail")]
  public string Email { get; set; }
}

Passing unnamed cascading values

To pass the unnamed IsDarkTheme cascading parameter to the <CascadingParams> component, do the following:

public class CascadingParams1Test : TestContext
{
  [Fact]
  public void Test()
  {
    var isDarkTheme = true;

    var cut = RenderComponent<CascadingParams>(parameters => parameters
      .Add(p => p.IsDarkTheme, isDarkTheme)
    );
  }
}

The example pass the variable isDarkTheme to the cascading parameter IsDarkTheme using the Add method on the ComponentParameterCollectionBuilder<TComponent> with the parameter selector to explicitly select the desired cascading parameter and pass the unnamed parameter value that way.

Passing named cascading values

To pass a named cascading parameter to the <CascadingParams> component, do the following:

public class CascadingParams2Test : TestContext
{
  [Fact]
  public void Test()
  {
    var cut = RenderComponent<CascadingParams>(parameters => parameters
      .Add(p => p.UserName, "Name of User")
    );
  }
}

The example pass in the value Name of User to the cascading parameter with the name LoggedInUser. Note that the name of the parameter is not the same as the property of the parameter, e.g. LoggedInUser vs. UserName. The example uses the Add method on the ComponentParameterCollectionBuilder<TComponent> with the parameter selector to select the cascading parameter property and pass the parameter value that way.

Passing multiple, named and unnamed, cascading values

To pass all cascading parameters to the <CascadingParams> component, do the following:

public class CascadingParams3Test : TestContext
{
  [Fact]
  public void Test()
  {
    var isDarkTheme = true;

    var cut = RenderComponent<CascadingParams>(parameters => parameters
      .Add(p => p.IsDarkTheme, isDarkTheme)
      .Add(p => p.UserName, "Name of User")
      .Add(p => p.Email, "[email protected]")
    );
  }
}

The example passes both the unnamed IsDarkTheme cascading parameter and the two named cascading parameters (LoggedInUser, LoggedInEmail). It does this using the Add method on the ComponentParameterCollectionBuilder<TComponent> with the parameter selector to select both the named and unnamed cascading parameters and pass values to them that way.

Rendering a component under test inside other components

It is possible to nest a component under tests inside other components, if that is required to test it. For example, to nest the <HelloWorld> component inside the <Wrapper> component do the following:

{
  [Fact]
  public void Test()
  {
    var wrapper = RenderComponent<Wrapper>(parameters => parameters
      .AddChildContent<HelloWorld>()
    );
    var cut = wrapper.FindComponent<HelloWorld>();
  }
}

The example renders the <HelloWorld> component inside the <Wrapper> component. What is special in both cases is the use of the FindComponent<HelloWorld>() that returns a IRenderedComponent<HelloWorld>. This is needed because the RenderComponent<Wrapper> method call returns an IRenderedComponent<Wrapper> instance, that provides access to the instance of the <Wrapper> component, but not the <HelloWorld>-component instance.

Configure two-way with component parameters (@bind directive)

To set up two-way binding to a pair of component parameters on a component under test, e.g. the Value and ValueChanged parameter pair on the component below, do the following:

@code {
  [Parameter] public string Value { get; set; } = string.Empty;
  [Parameter] public EventCallback<string> ValueChanged { get; set; }
}
public class TwoWayBindingTest : TestContext
{
  [Fact]
  public void Test()
  {
    var currentValue = string.Empty;

    RenderComponent<TwoWayBinding>(parameters =>
      parameters.Bind(
        p => p.Value,
        currentValue,
        newValue => currentValue = newValue));
  }
}

The example uses the Bind method to setup two-way binding between the Value parameter and ValueChanged parameter, and the local variable in the test method (currentValue). The Bind method is a shorthand for calling the the Add method for the Value parameter and ValueChanged parameter individually.

TestContext per test method

There are scenarios where it is not possible or not desirable to inherit from TestContext in the test class. In those cases, it is possible to create a new TestContext instance per test method. As the TestContext class implements IDisposable, it is recommended to use the using statement to ensure that the instance is disposed of after the test method has run.

public class HelloWorldExplicitContext
{
  [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>");
  }
}

Getting an InvalidOperationException

When the razor syntax is used and the test throws the following exception:

System.InvalidOperationException: The render handle is not yet assigned.

This usually means that the test class (Blazor component where the tests is declared in) is direclty inheriting from ComponentBase, as is the default for all Blazor components.

The solution is to inherit from bUnits TestContext instead, i.e.:

@inherits TestContext

@code {
    [Fact]
    public void Test1()
    {
        // test code
    }
}

Limitations of rendering a RenderFragment inside a test

When rendering a RenderFragment using the Render(RenderFragment) method, the created IRenderedFragment is static. This means that it will not re-render even if events are triggered.

@inherits TestContext

@code {
  [Fact]
  public void Button_clicked_string_gets_updated()
  {
    var output = string.Empty;
    var cut = Render(@<button @onclick='@(() => output = "Success")'>@output</button>);
    
    cut.Find("button").Click();
    
    // This will pass, as events are triggered and the function get executed
    output.ShouldBe("Success");

    // This will fail, as the markup will not get updated
    cut.Find("button").TextContent.ShouldBe("Success");
  }
}

Further Reading

Progress Telerik

Premium sponsor: Progress Telerik.

Packt

Editorial support provided by Packt.

.NET Foundation

Supported by the .NET Foundation.