Closed hassanhabib closed 2 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:
AddStub<Foo>
that creates a StubFoo
that does NOT inherit from Foo
AddXXX<Foo>
that creates a StubFoo
that does inherit from Foo
. If Foo
is sealed, the SG can return an error and tell user to use the other option, and perhaps a warning of Foo
has services injected into it. What XXX should be I am not sure.Speaking of naming, the current Stub
feature actually land somewhere between a stub and a spy, according to Martin Fowlers definitions.
Thoughts?
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.
@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:
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:
Okay now, with this long intro. Let me try to address your points:
Stub
and just call it Mock. much easier to understand and relate to. it does pretty much the same thing. Putting on my businessman hat here and saying, marketing this and it's usability and intuitiveness just based on the name is remarkable.
While the rest of the UI Framework world uses terms like stub, spy and some other terms, I think Mock is super efficient to convey the function with the least amount of learning curve risk possible.
bUnit is an intuitive library, let's keep that rhythm onwards. And since we're coming from C# with an audience who is predominantly back-end engineers - mock will make much more sense than Stub.
Now putting my tech hat back on - Fowler's definition of mock is perfectly fitting this situation. Base
components will have functions in them. These functions may need to behave a certain way. OnClick
wrapping like here for instance. I might not want to invoke a passed in function but rather something the external or native component is doing. Mocking is our way in. Imagine this code here: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.
@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.
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
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).
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
.
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.
@ref
problemConsider 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.
@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
@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 :)
@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.
@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.
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
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 asTextBoxBase
,DropDownBase
andButtonBase
.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 stubTextBoxBase
as long as I don't have a hard-defined property withTextBoxBase
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