Closed linkdotnet closed 10 months ago
Im thinking something slightly different.
Suppose we have a test where the user wants to stub/mock out the component, e.g.:
[Fact]
public void MyTest()
{
ComponentFactories.AddStub<TargetComponent>();
// ...
}
That is enough to signal that we should generate a stub/mock type for TargetComponent
. We can use a source generator to pick up all calls to AddStub
, pick out the TComponent
, and generate a partial TargetComponentStub
. That way users don't have to create partial class unless they have special needs.
If users want to customize the stub, they can create a public partial class TargetComponentStub { ... }
in their test project.
But, I think it is an opportunity to to create more than a stub. We could create a mock instead, where the user is able to configure the behavior and provide markup to be rendered.
But then, what about if users want to mock out all components from a namespace? Should we generate a whole bunch of mock types, which could be costly. That said, that would only happen once, or, when the user changes dependencies.
Another scenario is when users want to mock/stub their own types to isolate a component under test. Then the mockee may be changed pretty often and thus that should also influence the mock.
I will push my alpha version of the source generator as described initially.
The reason I don't want to use Stub is that I want to give the user the ability to partially override properties. That wouldn't work with the approach you described.
Would be interesting to pull in @scottsauber and ask how often, if ever, he uses mocks/stubs of components, and if he has any experience relevant to this.
I create a draft PR, including the first tests - there are MANY open questions. If you like I can annotate them to open up a discussion.
Would be interesting to pull in @scottsauber and ask how often, if ever, he uses mocks/stubs of components, and if he has any experience relevant to this.
Almost never. The React community used to do shallow rendering like this with the library Enzyme, but went away from it in favor of React Testing Library and deep rendering.
Basically using the guidance of “the more your tests resemble the way your users use your application, the more confidence they give you.”
More reading on that here - https://kentcdodds.com/blog/why-i-never-use-shallow-rendering
I will say it is annoying though when certain component libraries heavily use JS (ie Radzen does for its datepicker) instead of using pure C#. So that is a slight difference between bUnit and RTL, where RTL can run all code bc it’s all JS and bUnit can’t run any JS.
The reason I don't want to use Stub is that I want to give the user the ability to partially override properties. That wouldn't work with the approach you described.
Yes it would. And it does not require users to create a partial stub class, unless they have special needs. Most probably don't have that need, so it would lead to a whole bunch of empty partial classes just to create a stub.
Generate the default stub with the default implementation with a predictable/conventional name. Then, if users do create a partial class with the same name, that partial class is taken into consideration. If they create a partial class with a different name WITH an Stub
attribute, then that is name that is used.
I will say it is annoying though when certain component libraries heavily use JS (ie Radzen does for its datepicker) instead of using pure C#. So that is a slight difference between bUnit and RTL, where RTL can run all code bc it’s all JS and bUnit can’t run any JS.
@scottsauber, thanks for jumping in. Yes, this is the motivation for this functionality. Being able to remove 3rd party components that are causing problems and making it hard to test components in isolation.
Yes it would. And it does not require users to create a partial stub class, unless they have special needs. Most probably don't have that need, so it would lead to a whole bunch of empty partial classes just to create a stub.
But isn't that scenario covered even today? Just call AddStub
. So if you have no need, there will be still no need even with a generator.
So the goal, from my point of view, is to unify those two worlds, given the users more control with less conventions.
Yes it would. And it does not require users to create a partial stub class, unless they have special needs. Most probably don't have that need, so it would lead to a whole bunch of empty partial classes just to create a stub.
But isn't that scenario covered even today? Just call
AddStub
. So if you have no need, there will be still no need even with a generator.So the goal, from my point of view, is to unify those two worlds, given the users more control with less conventions.
The problem with the Stubobject
, but it is a bit convoluted to work with if you want to assert that specific parameters were passed to it.
With a generated stub/spy/mock, we could generate:
@ref
bindingFor example, if our CUT uses <MyButton>
(the component from your issue description) like this:
<p>Hey click my button: </p>
<MyButton Text="Go ahead click me" OnClick="() => clicked =true" />
<p>@clicked</p>
@code {
private bool clicked;
}
Then the generated mock could have a RenderTreeBuilder that results in the CUT producing the following markup:
<p>Hey click my button: </p>
<MyButton Text="Go ahead click me" OnClick="() => clicked =true" />
<p>false</p>
That is, the test double would more or less be able to generate the original razor markup in the component that declared it (not sure if we are able to capture lambdas passed to e.g. OnClick
).
We need an externally visible package for the Source Generator that is part of the bUnit family. That said, the Attribute and generator have to be part of said package,
Agreed.
If the user already implemented a (Cascading)Parameter, we shall skip the generation
If you mean that a user creates a partial test double, we only implement the parameters the user has not implemented, then yes, I agree.
We should respect the visibility of the class provided by the user (so if it is internal - we generate an internal one as well)
I think we should generate them as internal to the test project, independent of what the visibility is of the original component.
We only want Parameter or CascadingParameter for the start. .NET 8 introduced new ones like
SupplyParameterFromForm
, which we can add later on. Or shall we add them from the get-go?
The SupplyParameterFromForm
and SupplyParameterFromQuery
works as normal parameters in bUnit, since we don't have a HTTPContext with a HTTP request body or query parameter we map from. So not sure there is a need at all to support these parameters. Probably just create them as normal parameters.
v2 only?
I am not sure there are any specific things here that need V2. We can easily support this in V1 as well. But probably an optional package that's not included by default initially, like with bunit.web.query.
Should we make sanity checks for the implementing type? For starters I would always generate a stub that derives from ComponentBase.
What kinds of sanity checks are you thinking of?
Deriving from ComponentBase
may not add any value. Do we expect our stub to have a need for the life cycle methods? It probably just need to be able to receive new parameters whenever the parent/user component decides to pass some to it.
I am not sure how AddStub<ThirdPartyComponent>
is enough - sure we can create a new ThirdPartyComponentStub
- but that doesn't register it in the ComponentFactory - and that is something we can't really do with Source generators.
With the Attribute
- one can use it easily with AddStub<MyComp, MyCompStub>()
.
If you mean that a user creates a partial test double, we only implement the parameters the user has not implemented, then yes, I agree.
Yes, exactly.
Deriving from ComponentBase may not add any value.
Well, no need to implement anything. And we get SetParametersAsync
for free.
I think we should generate them as internal to the test project, independent of what the visibility is of the original component.
Well if it is partial - it has to have the same visibility. But if not given - yes internal
is absolutely enough.
detect if it is safe to inherit from the stubbed component and do so for the cases where users are using the @ref binding
That is a huge task! Given that we might into so many edge cases. I also don't feel that there is much benefit in even mimicking the markup - that might change with every version of a library, leaving the user at a place where he started.
I am not sure how AddStub
is enough - sure we can create a new ThirdPartyComponentStub - but that doesn't register it in the ComponentFactory - and that is something we can't really do with Source generators.
Interceptors in .net 8 allows us to do this.
But yeah, a different method none the less may be a good idea.
That is a huge task! Given that we might into so many edge cases. I also don't feel that there is much benefit in even mimicking the markup - that might change with every version of a library, leaving the user at a place where he started.
Yeah, without a doubt. Let's figure out what a MVP would be and do that. Then we can always add more on top of that.
I'm as always very inspired and ambitious 🙂
Just realized there is an annoying limitation with source generators. They cannot work on types created by other source generators in the target assembly. The Blazor compiler is a source generator. That means supporting this .razor test files is going to be a problem, as far as I can tell.
I do have a working version. Another limitation is that the user has to add:
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);MyFancyNamespace</InterceptorsPreviewNamespaces>
To their csproj file.
Anyway - I made it work with interceptors. There are many rough edges. I pushed it to the given branch
Big issues at the moment:
Calling AddStubGenerated
multiple times leads to duplicates(stubbing the same component in different tests), and I am not sure if there are ways around that (besides ugly random numbers at the interceptors class name), as generators can't see the output of other generators, as you said.
That said - the attribute version is the most reliable one at the moment from my point of view.
Big issues at the moment:
Calling
AddStubGenerated
multiple times leads to duplicates(stubbing the same component in different tests), and I am not sure if there are ways around that (besides ugly random numbers at the interceptors class name), as generators can't see the output of other generators, as you said.That said - the attribute version is the most reliable one at the moment from my point of view.
Keep track of generated types in a hashset?
Got it working with multiple - I needed a better understanding of the Generator API.
In v1
of the generator, I would like to concentrate on the 80% use case - that is, having no partial stub around that we have to take care of.
With that I am pretty far and have to patch out some rough edges.
I also updated the description to reflect the latest discussions
Rationale & Motivation
When dealing with 3rd party components, often times users want to fake out those to have stable and deterministic tests. As those 3rd party components are still important to trigger certain events in the users code base - they are forced to create stubs manually.
So if we have a look at the following
<MyButton>
component:The user is faced with two choices:
And use
ComponentFactories
to use said stub instead of the 3rd party library.Here we can help the user so that we automatically create the public surface API of a component via a generator. The use case might look like this:
The generator should generate a class that looks like the one above. This makes it way easier for users to generate stable tests independent of external factors that aren't under the control of the user.
Considerations
Parameter
orCascadingParameter
for the start. .NET 8 introduced new ones likeSupplyParameterFromForm
, which we can add later on. Or shall we add them from the get-go?v2
only?ComponentBase
.I created an experimental branch: https://github.com/bUnit-dev/bUnit/tree/feature-stub
Update
An alternative approach, we are following is to utilize Interceptors that were introduced with .NET 8 and C# 12. Instead of the attribute the user can do this:
From the point of view of the user, there is no need to create
CounterComponentStub
in the first place. This will be handled by the generator. It basically behaves likeComponentFactories.Add<CounterComponent, CounterComponentStub>()
with the public surface "mirrored".To make that work we introduce a new function
AddGeneratedStub
. It is convention based in the sense of that we just addStub
as suffix to the component.Limitations are: