bUnit Generators - Generate your stubs automatically

17/12/2023
BlazorbUnit

We made a recent addition to bUnit that we think will make your life easier. We call it bUnit Generators. It's a set of code generators that will help you create stubs for your third-party components.

The problem

When you write unit tests, you try to be as isolated as possible. You want to test your component in isolation, without any dependencies. But then the real world hits - and your components aren't in isolation at all. Let's face it: The majority of (enterprise) apps rely on third-party components. How likely is it to create a component library on your own?

Back to "isolation": Why do you want to isolate your component? Mainly because those third-party components are not under your control. They might change their API, they might have bugs, and they might not work as expected. You don't want to test their code; you want to test yours. (And you don't get paid/spend your resources on testing their code.)

So when we do this in Blazor and bUnit, the normal way is via stubbing components. Until now, you could do the following:

[Fact]
public void MyTest()
{
    ComponentFactories.AddStub<ThirdPartyComponent>();
    // ...
}

This will add an empty stub for the ThirdPartyComponent to the test context. In many cases that is nice and you are done! But sometimes you want to assert or check what is passed into those components. And that's where it gets tricky. Here is an example of doing so. Imagine our ThirdPartyComponent has a Counter parameter that is of type int and we want to check that it is set to 42:

[Fact]
public void MyTest()
{
    ComponentFactories.AddStub<ThirdPartyComponent>();
    // ...

    var stub = cut.FindComponent<Stub<ThirdPartyComponent>>();
    stub.Instance.Parameters.First(s => s.Key == "Counter").Value.Should().Be(42);
}

That isn't very intuativ and also doesn't work if they change the public API. So we need a better way to do this.

Meet: Source code generators

Before we go into some examples and code, let's talk about what, in the majority of cases, you want from a stub. The stub should mimic the original component as closely as possible. That means it should have the same public API. And that means in the majority of cases, you want to mimic the public parameters (including cascading ones). And that's what we do with the source code generators.

And we are offering with bUnit.generators: Generating stubs for you. And we are offering two ways:

  1. Via the new interceptor feature introduced with .net 8
  2. Via a new attribute

In both cases, you want to add the following package to your test project

dotnet add package bunit.generators

Interceptor

The interceptor is a new feature introduced with .net 8. It allows you to intercept calls to methods and properties. To make it work, we first have to enable the new interceptor feature. For this we have to adapt the csproj file of our test project. We have to add the following lines to the PropertyGroup section:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <!-- ... -->
        <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Bunit</InterceptorsPreviewNamespaces>
    </PropertyGroup>

Now what the interceptor does is to intercept the call to AddStub<TComponent>() and generate the stub for you. So our test from above looks like this:

[Fact]
public void MyTest()
{
    // ...
    ComponentFactories.AddStub<ThirdPartyComponent>();
    // ...

    var stub = cut.FindComponent<ThirdPartyComponentStub>();
    stub.Instance.Counter.Should().Be(42);
}

Two main differences:

  1. We are using the ThirdPartyComponentStub instead of the Stub component - that is the component the generator created for us.
  2. We have strongly typed stubs that mimic the public API of the original component.

The whole chain starts with the AddStub method that gets intercepted. The interceptor then generates the stub for us. And that's it. We can now use the strongly typed stubs in our tests.

You can check out the generated class - in our case, it looks like this:

namespace MyNamespace;

internal partial class ThirdPartyComponentStub : global::Microsoft.AspNetCore.Components.ComponentBase
{
	[global::Microsoft.AspNetCore.Components.ParameterAttribute]
	public int Counter { get; set; }
}

The partial modifier allows us to extend the class with our own code. So we can add our own logic to the stubs. That is especially helpful if you want to mimic some other behavior like public methods.

As the interceptor is still in preview and needs some setup, we also introduced a new attribute that does partially the same:

Attribute

The attribute is a marker to generate a stub. The test from above would look like this

[Fact]
public void MyTest()
{
    // ...
    ComponentFactories.Add<ThirdPartyComponent, ThirdPartyComponentStub>();
    // ...

    var stub = cut.FindComponent<ThirdPartyComponentStub>();
    stub.Instance.Counter.Should().Be(42);
}


[ComponentStub<ThirdPartyComponent>)]
internal partial class ThirdPartyComponentStub;

The attribute is a marker that tells the generator to generate a stub for the ThirdPartyComponent. The generator will then generate a stub with the name ThirdPartyComponentStub. The ComponentFactories.Add method then adds the stub to the test context. The rest is the same as above.

Conclusion

The features I showed are still "in preview". We are still working on them and will improve them over time. But we think they are already very helpful and will make your life easier. If you have any feedback, please let us know. Head over to the GitHub repository (https://github.com/bUnit-dev/bUnit) and let us know your thoughts!

Resources

0
An error has occurred. This application may no longer respond until reloaded. Reload x