bUnit-dev / bUnit

bUnit is a testing library for Blazor components that make tests look, feel, and runs like regular unit tests. bUnit makes it easy to render and control a component under test’s life-cycle, pass parameter and inject services into it, trigger event handlers, and verify the rendered markup from the component using a built-in semantic HTML comparer.
https://bunit.dev
MIT License
1.15k stars 108 forks source link

FEATURE: Sub-Components Mocking/Stubbing #560

Closed hassanhabib closed 2 years ago

hassanhabib commented 3 years ago

Scenario: When I develop my Blazor components, I try to break them into three types. Pages, Base and Components. Base Components are wrappers around native or external UI components such as Syncfusion SfGrid or native HTML label control. Components utilize these Base Components to build a more business-focused capability. For instance, a Student Registration Component would look like this:

StudentRegistrationComponent.razor.cs

....
    public partial class StudentRegistrationComponent : ComponentBase
    {
        [Inject]
        public IStudentViewService StudentViewService { get; set; }

        public ComponentState State { get; set; }
        public StudentRegistrationComponentException Exception { get; set; }
        public StudentView StudentView { get; set; }
        public TextBoxBase StudentIdentityTextBox { get; set; }
        public TextBoxBase StudentFirstNameTextBox { get; set; }
        public TextBoxBase StudentMiddleNameTextBox { get; set; }
        public TextBoxBase StudentLastNameTextBox { get; set; }
        public DropDownBase<StudentViewGender> StudentGenderDropDown { get; set; }
        public DatePickerBase DateOfBirthPicker { get; set; }
        public ButtonBase SubmitButton { get; set; }
        public LabelBase StatusLabel { get; set; }

        .....

    }

In the code above, I have properties that represent injected dependencies such as StudentViewService and I also have dependencies that represent localized UI components such as TextBoxBase, DropDownBase and ButtonBase.

The ask here is to be able to stub these properties the exact same way I can mock StudentViewService. Today the stubbing capability can allow you to stub if and only if you don't have a local property with the type. For instance, I can stub TextBoxBase as long as I don't have a hard-defined property with TextBoxBase in it.

This can be problematic, because I need to be able to access these properties programmatically via the instance of the rendered component to either execute more functionality like Grid.DisplayLoading() for instance. In order for that to happen a @ref needs to be established on the component level so we can have that kind of control.

The alternative is to use .FindComponents<T> which is very inconvenient, especially if you have multiple components with the same type, it starts to get a bit less performant.

This here is an example of this issue: https://github.com/hassanhabib/OtripleS.Portal/blob/users/hassanhabib/poc-stub-component-property/OtripleS.Portal.Web.Tests.Unit/Views/TeachersComponents/TeachersComponentTests.Render.cs#L45

egil commented 3 years ago

Hi @hassanhabib,

The problem here is creating a solution that works in all cases, that wont confuse users.

My first thought was to create source generator that generators a Stub of a component.

E.g. for Foo component:

public class Foo : ComponentBase
{
  [Parameter] public string Name { get; set; }
}

Then, in a test:

[Fact]
public void UserOfFooTest()
{
  ComponentFactories.AddStub<Foo>(); // this signals to the SG to generate StubFoo
  // ...
}

The SG would generate StubFoo:

internal class StubFoo : Foo 
{
  [Parameter] public string Name { get; set; }
  // additional overrides of life cycle methods
}

StubFoo should also override all life-cycle methods of the Foo, effectively neutering it so none of it's logic is triggered by the Blazor runtime. There is some extra leg work to be done here to deal with Dispose, DisposeAsync and default constructors, if the Foo target uses these.

However, this would make StubFoo a Foo, meaning the @ref problem would be fixed, and you could more easily verify the values passed to each parameter.

The problem with this approach is that it not allow me to override dependencies on injected services in Foo, .e.g.:

public class Foo : ComponentBase
{
  [Inject] public SomeService Service { get; set; }
  [Parameter] public string Name { get; set; }
}

In this case, the Blazor runtime will still inject SomeService into StubFoo if Foo, and bUnit is not able to hook into this part of the Blazor runtime, meaning that users would have to provide a SomeService to the DI container in bUnit to render. And if that SomeService is not easy to come by, that is not ideal.

Second approach is to use a SG to generate a StubFoo that does NOT inherit from Foo. That doesn't solve the "is a" @ref problem though, but does solve the other problems. This also handles the cases where the source component is sealed.

I am think that both options should be included:

Speaking of naming, the current Stub feature actually land somewhere between a stub and a spy, according to Martin Fowlers definitions.

Thoughts?

egil commented 3 years ago

This can be problematic, because I need to be able to access these properties programmatically via the instance of the rendered component to either execute more functionality like Grid.DisplayLoading() for instance. In order for that to happen a @ref needs to be established on the component level so we can have that kind of control.

With a mocked/stubbed component, you would not be able to call methods on the stub/mock

The alternative is to use .FindComponents which is very inconvenient, especially if you have multiple components with the same type, it starts to get a bit less performant.

.FindComponents<T> is verbose. I would love to have an easier way to traverse the render tree/component tree. I have yet to figure out a good way to create the render tree as the renderer renders and maintain it while it re-renders.

As for performance, it should not be any worse, so not sure I understand the bit less performant bit of your remark.

hassanhabib commented 2 years ago

@egil thank you brother I appreciate your feedback.

Let me roll-back a bit and just try to clarify few things. Any dependency of non-localized models or functionality should be wrapped up and abstracted away in some component. I call those components Brokers for data pipelines and Base Components in UI development. The idea here is that at any point in time, if I decide to switch from Syncfusion to Telerik on the UI side, or from SendGrid to MailMonkey on the data side I should do zero changes to my core business logic. These are design decisions that I have outlined a bit more in detail in The Standard - Specifically the Brokers section.

Here's a visualization of how I architect these Blazor enterprise applications:

image

With the data pipeline case, I can easily mock my wrappers (brokers) using their interface/contract. I don't want to be making real database calls therefore I mock that wrapper so I can only test my core localized logic.

I want to be able to achieve the same thing with UI. I don't want to actually call Syncfusion or Telerik when I'm running my unit tests. The problem here is that we can't create contracts for Base Components. A while back Josh McCall (a friend) proposed treating base components as services, and injecting them into the components so your code would look like this:

[Inject]
public IStudentService StudentService {get; set;}

[Inject]
public ITextBoxBase TextBoxBase {get; set;}

The solution looks very appealing at the first glance. However, it requires a hack to pretend that our Blazor base components are brokers. They are not. They don't liaison between an external service, they just wrap up external or native UI components.

For a while there I thought maybe I should just create an extension for the Services.AddTransient piece in ASP.NET Core and create Components.AddTransient But I still thought this was a bit hacky, since base components are very different than brokers in their very nature, even though they perform the exact same functionality conceptually in terms of abstraction.

This went on for almost a year until you introduced Stubbing to me and Mr. Parker. So let me just clarify these two things:

  1. I do understand that your implementation of stubbing here tries to replace the existing component with another, where the type becomes something completely different. Totally get it. This is the tough part.
  2. I don't think we should let technology bend our vision, it should be the other way around - this is why we are having this amazing technical discussion right now.

Okay now, with this long intro. Let me try to address your points:

private readonly BMock<ButtonBase> buttonBaseMock;

public MyTestClass()
{
     this.buttonBaseMock = new BMock<ButtonBase>();
}

[Fact]
public void ShouldSubmitStudentAsync()
{
     // given
     this.buttonBaseMock.Setup(baseButton =>
          baseButton.Click())
              .Returns(Whatever);

     // when

     // then
     this.buttonBaseMock.Verify(baseButton =>
          baseButton.Click(),
              Times.Once);
}

First of all, BMock is something I just came up with, and I think it's pretty cool name haha.

Secondly, whatever the button is hiding behind is not even mentioned in here. I could have a Syncfusion button behind that base and I don't have to worry about setting up anything for that. If I switch tomorrow to Telerik, nothing in this code is going to change at all - that I call a perfect engineering experience at an enterprise-level.

Third point here is the verification and setup of any interactions with this component. Now I can control what happens next. Here's a real-life example. Say I have a GridBase and I want to know that my StudentsComponent is kicking off the DisplayLoading() capability when the component is still in Loading state. Now what happens? I want to verify that my StudentComponent actually did that. I can now go and verify this have actually happened and only one time before loading the data. See the possibilities here? More importantly the coherence from back to front-ends which is what Blazor is all about.

Fourth point, I feel that we might be inching outside of the realm of a testing framework. Should BMock be it's own library? like Moq? (would LOVE to work with you on that though, but I'm not smart enough haha).

Fifth point here, I'm not worried about the injected services into base components. If you are mocking a base component, you are already saying nullify everything within that component unless I set it up otherwise. And what I would only worry about is the APIs the base component is exposing not what it's using underneath to execute the function of these APIs. You know what I mean? If a StorageBroker is calling DbContext underneath. And it's injected from somewhere - who cares? I'm already overriding all of that. Not a concern for me.

I feel like I said too much, should we go live and discuss this publicly? :)

Lastly, this feature here is not an easy task. The Blazor community is lucky to have great minds like yourself to pave the way in these uncharted waters. A friendly reminder that you are an awesome human being @egil . Thank you for your amazing work and the passion you bring every day. Let's keep thinking about this.

I'm pulling in some of my favorite people in this realm to think out loud with us. @danroth27 && @SteveSanderson - your feedback here is also greatly, greatly appreciated gentlemen.

egil commented 2 years ago

@hassanhabib, thank you for explaining the pattern are using and motivation behind.

To generalize though, what you are after is a way to tell the Blazor Renderer to instantiate and render component X instead of component Y. Let me explain the "hook" there is in the Blazor Renderer for controlling this, hopefully that can help guide the discussion further.

Blazors and bUnits hook into component instantiation

The Blazor Renderer uses a IComponentActivator to instantiate components while it is walking down the render tree. bUnit comes with a its own IComponentActivator, the BunitComponentActivator.

BunitComponentActivator will use the IComponentFactory added to TestContext.ComponentFactories to decide what should be instantiated, when the Blazor Renderer askes to get an instance of Component X. How this works is described here: https://bunit.dev/docs/providing-input/controlling-component-instantiation

When do Blazor instantiates components

The Blazor Renderer will walk down the render tree (via BuildRenderTree/RenderFragment) and only ask the BunitComponentActivator to instantiate a component when it sees it. Consider this render tree:

<Component>
  <Base>
    <ThirdPartyCompoent>
      <SubCompoent/>
    </ThirdPartyCompoent>
  </Base>
</Component>

The renderer will first instantiate <Component>, then discover through its BuildRenderTree method that there is a child <Base> component, which will then be instantiated, and so on and so forth.

If we do not add any IComponentFactory's to bUnit, all components will be instantiated one at the time, until the renderer reaches the bottom of the render tree.

If however we add a IComponentFactory, whose CanCreate method returns true for typeof(ThirdPartyCompoent), and its Create method returns a <MockThirdPartyCompoent> component, then <SubCompoent> is never rendered, assuming <MockThirdPartyCompoent> doesn't include it inside its render tree. In keeping with the tree metaphor, we are cutting off a branch and adding a completely different one instead, that may or may not have its own sub branches.

In most cases, Blazor doesn't care that ThirdPartyCompoent is replaced with a completely different type/component, as long as Blazor can pass the parameters to the instance of the type/component if has been given. First, Blazor will try to set the parameters on properties with matching names. All parameters it cannot find a matching property for, it will try pass to a property with the [Parameter(CaptureUnmatchedValues = true)] attribute on. If no such property exits, it will throw an exception.

However, if <Base> includes an @ref to <ThirdPartyCompoent> in its code, then the type/component returned by the component factory must be assignable to ThirdPartyCompoent, i.e. inherit from it, otherwise we get an exception as well.

So, in my mind, the first thing we need to decide is how to create mocks/replacement components. Then we can decide on what features we might want to support in a mock/replacement component (e.g. describing expectations, setting up calls, as you, @hassanhabib, show in the BMock example).

Current solution

The current implementation of a replacement component, Stub<T>, just captures all parameters passed to a component in a "CaptureUnmatchedValues" parameter. That allows us to assert that the right parameters was passed to the component we're stubbing, however, as you have discovered, we cannot assign Stub<ThirdPartyCompoent> to an @ref that expects a ThirdPartyCompoent.

egil commented 2 years ago

Regarding creating something like BMock with features such as Setup and Verify, I would prefer not to actually.

There are numerous mocking frameworks already like Moq, NSubstitute, JustMock, each with their own syntax. Copying one syntax from e.g. Moq to bUnits mock lib is not ideal for users who prefer another syntax.

Instead I would like to explore a way to allow users to combine their favorite mocking framework with bUnits ability to control component instantiation, ideally generalized such that bUnit doesn't have to have explicit support for e.g. Moq or NSubstitute if possible.

egil commented 2 years ago

Thoughts on @ref problem

Consider the following component under test:

<ThirdPartyCompoent @ref="thridPartyComponent" />
@code {
  private ThirdPartyCompoent thridPartyComponent;
  protected override void OnInitialzed()
  {
    var something = thridPartyComponent.GetSomething();
    // do something with something
  }
}

And our ThirdPartyCompoent look like this:

public class ThirdPartyCompoent : ComponentBase
{
  public Something GetSomething() { }
}

The problem here is that ThirdPartyCompoent is inherently unmockable. It's a class, where its GetSomething method is not virtual, something I assume will be quite common.

We can create (generate) a MockThirdPartyCompoent that look like this:

public class MockThirdPartyCompoent : ThirdPartyCompoent
{
  // ... overrides of all life-cycle methods not included for brevity
  public new Something GetSomething() { } // mock behavior added here through magic 
}

However, when our component under test calls thridPartyComponent.GetSomething(), it wont hit the mocks version of the GetSomething method, it will hit the GetSomething in the ThirdPartyCompoent.

I dont see a way around that, as long as methods in the original component is not virtual. Expecting components from 3rd party component vendors to be virtual is not realistic. They might even created sealed components, that completely remove the possibility of creating a mock that inherits from the original.

If the mocked component is something you own yourself, then you can obviously make your methods virtual, and then mocking will work.

egil commented 2 years ago

@hassanhabib, I have played around with integrating Moq and NSubstitute with bUnit, and I have something I would like you to take a look at, since it seem to work surprisingly well.

To get started, add the following ComponentFactoryCollectionExtensions.cs file to your bUnit test project:

using System;
using Microsoft.AspNetCore.Components;

namespace Bunit.TestDoubles;

public static class ComponentFactoryCollectionExtensions
{
    public static void AddMock<T>(this ComponentFactoryCollection factories, T mock)
        where T : IComponent
    {
        factories.Add(new MockInstanceComponentFactory(typeof(T), mock));
    }

    public static void AddMock<T>(this ComponentFactoryCollection factories, Func<T> mockFactory)
        where T : IComponent
    {
        factories.Add(new MockComponentFactory<T>(mockFactory));
    }

    private class MockInstanceComponentFactory : IComponentFactory
    {
        private readonly Type target;
        private readonly IComponent mockInstance;

        public MockInstanceComponentFactory(Type target, IComponent mockInstance)
        {
            this.target = target;
            this.mockInstance = mockInstance;
        }

        public bool CanCreate(Type componentType) => target == componentType;

        public IComponent Create(Type componentType) => mockInstance;
    }

    private class MockComponentFactory<T> : IComponentFactory where T : IComponent
    {
        private readonly Type target;
        private readonly Func<T> mockFactory;

        public MockComponentFactory(Func<T> mockFactory)
        {
            this.target = typeof(T);
            this.mockFactory = mockFactory;
        }

        public bool CanCreate(Type componentType) => target == componentType;

        public IComponent Create(Type componentType) => mockFactory.Invoke();
    }
}

The two extension methods in this class makes it easy to add simple component factories that will either return the same mock object for every component of a specific type (singleton mock), or use the mock factory to create a new mock for every component of a specific type (trancient mock).

To use while testing the following <Component1> component:

<ThirdPartComp @ref="c" Value="FOO BAR BAZ" />
@value
@code {
    private ThirdPartComp c;
    private string? value;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            value = c.GetVirtual();
            StateHasChanged();
        }
    }
}

in which the following <ThirdPartComp> component is mocked:

<p>@GetVirtual()</p>
<p>@GetNonVirtual()</p>
<p>@Value</p>

@code {
    [Parameter] public string Value { get; set; }

    public virtual string GetVirtual() => $"{nameof(ThirdPartComp)}.{nameof(GetVirtual)}";
    public string GetNonVirtual() => $"{nameof(ThirdPartComp)}.{nameof(GetNonVirtual)}";
}

we can do the following, using Moq:

using Bunit;
using Bunit.TestDoubles;
using FluentAssertions;
using Microsoft.AspNetCore.Components;
using Moq;
using Xunit;

public class UnitTest1 : TestContext
{
    [Fact]
    public void Test1()
    {
        var mockComp = new Mock<ThirdPartComp>();
        mockComp.Setup(comp => comp.GetVirtual()).Returns("MOCK");
        ComponentFactories.AddMock<ThirdPartComp>(mockComp.Object);

        var cut = RenderComponent<Component1>();

        cut.MarkupMatches("MOCK");
    }

    [Fact]
    public void Test1_3()
    {
        var mockComp = new Mock<ThirdPartComp>();
        mockComp.Setup(comp => comp.GetVirtual()).Returns("MOCK");
        mockComp.Setup(c => c.SetParametersAsync(It.IsAny<ParameterView>()))
            .Callback<ParameterView>(pv => pv.SetParameterProperties(mockComp.Object));

        ComponentFactories.AddMock<ThirdPartComp>(mockComp.Object);

        var cut = RenderComponent<Component1>();

        mockComp.Object.Value.Should().Be("FOO BAR BAZ");
    }
}

The last test shows how you can set parameters on the mock object itself and verify them.

Take-aways: This works because Moq (and NSubstitute) by default overrides all virtual methods, meaning none of the Blazor life-cycle methods do anything. So unless you set up things for a components virtual methods, including its Blazor life-cycle methods, the component simply outputs nothing and does nothing, which is a good default I think.

The last test sets the parameters on the mocked object, i.e. in this case the Value parameter. This might not be a good thing if the mocked objects have parameter properties that are more than just auto implemented get; set; properties. In that case, a better approach would simply be to capture the ParameterView passed to SetParametersAsync and verify against that.

Another positive thing about this approach is that it doesn't really require us building source generators or similar things to generate mock objects. This utilizes existing mocking libraries which means less code to maintain.

Let me know if you see any problems with this approach, and if there are things we need to consider or can improve upon!

See attached zip for the full playground: MockingIntegrationSample1.zip

hassanhabib commented 2 years ago

@egil Thank you so much for this. This is beyond beautiful. There's only one tiny piece that would be this perfect! Verifying a particular dependency component have had certain parameters passed to it. here's my scenario:

Our TeachersComponent uses a dependency base component GridBase<T>.

The TeachersComponent is calling a service to pull in the data like this:

        protected async override Task OnInitializedAsync()
        {
                this.TeacherViews =
                    await this.TeacherViewService.RetrieveAllTeachersAsync();
        }

On the markup side of the component we go and pass the TeacherViews parameter to the GridBase<TeacherView> component like this:

    <GridBase @ref=Grid DataSource=TeacherViews />

The extension you created does a wonderful job turning the Grid property on the component class into a mocked component and we don't have to worry about any third party dependency - beautiful!

But now my test is trying to verify that the right data source has been passed to that mocked component. Something like this:

            this.renderedTeachersComponent.Instance.Grid.DataSource
                .Should().BeEquivalentTo(expectedTeacherViews);

Now, this part doesn't recognize any values being passed into that base component. Ideally, with our mocked base components we want to be able to do the following:

If we can work out verifying passed in parameters from the component side not from the test side, that'd be it my friend - you are a genius btw :)

egil commented 2 years ago

@hassanhabib thanks for the feedback. The current solutions support (at least) these two ways of verifying parameters passed to a mocked component.

The first approach:

[Fact]
public void Sets_Parameters_On_Mock_Object()
{
    var mockComp = new Mock<ThirdPartComp>();
    mockComp.Setup(c => c.SetParametersAsync(It.IsAny<ParameterView>()))
        .Callback<ParameterView>(pv => pv.SetParameterProperties(mockComp.Object));

    ComponentFactories.AddMock<ThirdPartComp>(mockComp.Object);

    var cut = RenderComponent<Component1>();

    mockComp.Object.Value.Should().Be("FOO BAR BAZ");
}

In the first test we use Blazor's ParameterView.SetParameterProperties to set any parameters pass to the mocked component on the mocked components parameter properties. this works, but If the mocked components parameter are not virtual, the original components properties are called, which might invoke other non-virtual (unmockable) code.

If the parameters properties in the mocked component are virtual, then this approach is safe, and you can simply verify the properties being set directly through the component like in the test above, or through Moq's Verify methods, like with a regular mocked interface. So if you control the component you are mocking, just make parameters properties and methods virtual, and this should work very well.

The second approach:

[Fact]
public void Captures_Parameters_Passed_To_Mock_Object()
{
    IReadOnlyDictionary<string, object> capturedParameters = default;
    var mockComp = new Mock<ThirdPartComp>();
    mockComp.Setup(c => c.SetParametersAsync(It.IsAny<ParameterView>()))
        .Callback<ParameterView>(pv => capturedParameters = pv.ToDictionary());

    ComponentFactories.AddMock<ThirdPartComp>(mockComp.Object);

    var cut = RenderComponent<Component1>();

    capturedParameters[nameof(ThirdPartComp.Value)].Should().Be("FOO BAR BAZ");
}

The second test just captures the parameters passed to the mock object, converts it to a Dictionary, which we can then assert against. This ensures that no non-virtual methods on the mocked component is not invoked. We have to convert the ParameterView to a Dictionary because the ParameterView is disposed/cannot be accessed, after the RenderComponent call is done.

To make our test simpler, we can then do the following (Moq based) extension methods:

public static class ComponentFactoryCollectionExtensions
{
    public static Mock<T> AddMoqMock<T>(this ComponentFactoryCollection factories)
        where T : class, IComponent
    {
        var mockComp = new Mock<T>();

        mockComp.Setup(c => c.SetParametersAsync(It.IsAny<ParameterView>()))
            .Callback<ParameterView>(pv => pv.SetParameterProperties(mockComp.Object));

        factories.AddMock<T>(mockComp.Object);

        return mockComp;
    }

    public static (Mock<T> Mock, IReadOnlyDictionary<string, object> Parameters) AddMockWithParamsCapture<T>(this ComponentFactoryCollection factories)
        where T : class, IComponent
    {
        var result = (Mock: new Mock<T>(), Parameters: new Dictionary<string, object>());

        result.Mock.Setup(c => c.SetParametersAsync(It.IsAny<ParameterView>()))
            .Callback<ParameterView>(pv =>
            {
                foreach (var p in pv.ToDictionary())
                {
                    result.Parameters[p.Key] = p.Value;
                }
            });

        factories.AddMock<T>(result.Mock.Object);

        return result;
    }

    // ...
}

Then the two tests above looks like this:

 [Fact]
    public void Sets_Parameters_On_Mock_Object()
    {
        var mockComp = ComponentFactories.AddMoqMock<ThirdPartComp>();

        var cut = RenderComponent<Component1>();

        mockComp.Object.Value.Should().Be("FOO BAR BAZ");
    }

    [Fact]
    public void Captures_Parameters_Passed_To_Mock_Object()
    {
        var (mockComp, capturedParameters) = ComponentFactories.AddMockWithParamsCapture<ThirdPartComp>();

        var cut = RenderComponent<Component1>();

        capturedParameters[nameof(ThirdPartComp.Value)].Should().Be("FOO BAR BAZ");
    }

At this point, we're entering into the mocking frameworks extensibility area, and its something I would prefer to avoid adding to bUnit's core libs. However, perhaps a (community driven) bunit.extensions.moq lib could exists that add these or similar extensions that works to make mocking easier.

egil commented 2 years ago

@hassanhabib, the additions that are part of the preview release should allow you to use your favorite mocking framework quite easily with bUnits, so I'll close this, and we can open new issues if there are specific additional features needed.