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.13k stars 105 forks source link

Enable adding parent components and cascading values to test contexts #155

Closed egil closed 3 years ago

egil commented 4 years ago

Some test scenarios requires one or more non-test-specific parent components or cascading values to make the test possible.

An example would be stuff that would normally be placed in the App.razor file, e.g. <CascadingAuthenticationState> or <TelerikRootComponent>. These are not unique to the individual test, but they to be there to make components work correctly, i.e. they are sort of a cross-cutting concern.

This would also allow component vendors and test doubles to register these as needs, similar to services. E.g. for the #151, we could have a single ctx.AddTestAuthorization() method call, that adds both services and parent component in one method call.

It is also useful in razor based tests, since it would remove the added noise from the non-test-specific components in the razor code.

My idea is to have something similar to Services on ITestContext, such that this is possible:

var ctx = new TestContext();

ctx.CascadingValues.Add(new MyValue()); // unnamed cascading value
ctx.CascadingValues.Add("named cascading value", new MyValue()); // named cascading value

// parent components has to have a ChildContent parameter
ctx.ParentComponents.Add<CascadingAuthenticationState>(); 
ctx.ParentComponents.Add<MyParentComponentWithParams>(parameters => parameters.Add(p => p.SomeParam, 42));

// Renders MyComponent inside two cascading values and 
// a CascadingAuthenticationState and a MyParentComponentWithParams. In razor syntax, that would be:
// <CascadingValue Value=@(new MyValue())>
//   <CascadingValue Name="named cascading value" Value=@(new MyValue())>
//     <CascadingAuthenticationState>
//        <MyParentComponentWithParams SomeParam="42">
//           <MyComponent>
var cut = ctx.RenderComponent<MyComponent>();

Render tree order rules:

  1. CascadingValues are added to the render tree before ParentComponents
  2. ParentComponents are added as children of CascadingValues
  3. Both CascadingValues and ParentComponents are added in the order they are added to the test context in.

Should there be a way to override the implicit orderinger?

CascadingValues methods:

ParentComponents methods:

Edge cases:

  1. User adds a parent component of the same type as component under test. RenderComponent call has to return the nested instance, not the parent.
mrpmorris commented 4 years ago

I like CascadingValues.Add (or perhaps AddCascadingValue(....) instead.

I too dislike RootComponents.Add, what about WrapWithComponent<T>() and WrapWithComponent(IComponent) or EncloseWithinComponent?

Or perhaps stick with an Add* theme and go for something like AddCascadingValue and AddParentComponent

egil commented 4 years ago

I like CascadingValues.Add (or perhaps AddCascadingValue(....) instead.

I think having it as a collection like type (CascadingValues) opens up to other possibilities, such seeing what values are registered, in a cleaner way.

I too dislike RootComponents.Add, what about WrapWithComponent<T>() and WrapWithComponent(IComponent) or EncloseWithinComponent?

Similarly, I think I prefer a collection like type for "components that should wrap the CUT" components for consistency with Services and CascadingValues. Is WrapperComponents better than RootComponents?

SQL-MisterMagoo commented 4 years ago

ParentComponents might be more in line with normal terminology - e.g. when people are building a Tab control, they would have a Parent rather than a Root or a Wrapper

egil commented 4 years ago

@SQL-MisterMagoo I like ParentComponents better. You are right, it fits the terminology. A parent in a tree like structure like the render tree is the node above the current node. The current node being cut.

egil commented 4 years ago

@SQL-MisterMagoo / @mrpmorris added some additional notes about ordering and methods. What do you think?

mrpmorris commented 4 years ago

The only thing that is unclear is if ParentComponents.Add adds it inside the last added parent, or as the parent of the last added parent - which is why I liked WrapWithComponent type method names

egil commented 4 years ago

The only thing that is unclear is if ParentComponents.Add adds it inside the last added parent, or as the parent of the last added parent - which is why I liked WrapWithComponent type method names

@mrpmorris can you show how that would look Peter? I do not see how two calls to WrapWithComponent make that more clear than two calls to ParentComponents.Add do.

mrpmorris commented 4 years ago

Pseudo code using html element names instead of component names

ctx.ParentComponents.Add("p");
ctx.ParentComponents.Add("div");

When I test my component, which will I get

A: In the order they appear in the code?

<p>
  <div>
    <TestSubject/>
  </div>
</p>

If so, then you are actually adding a child to the previous parent

B: In the reverse order?

<div>
  <p>
    <TestSubject/>
  </p>
</div>

If so, the order being opposite to the code is confusing because you have to read the code backwards to understand the resulting component structure

The following code

ctx.WrapWithComponent("p");
ctx.WrapWithComponent("div");

To me clearly means 1: Wrap the component with p 2: Then wrap that with div

But is still a bit back to front, because you have to read the code from bottom to top again - but at least I expect it now.

<div>
  <p>
    <TestSubject/>
  </p>
</div>

How about something like HostingComponents?

ctx.HostingComponents.Add("p");
ctc.HostingComponents.Add("div");

To me this says 1: I want to add a p to host whatever comes after 2: Then I want to add a div inside that host, to host whatever comes after 3: Then I want my component inside the most recent host

egil commented 4 years ago

Pseudo code using html element names instead of component names

ctx.ParentComponents.Add("p");
ctx.ParentComponents.Add("div");

When I test my component, which will I get

A: In the order they appear in the code?

<p>
  <div>
    <TestSubject/>
  </div>
</p>

If so, then you are actually adding a child to the previous parent

This is the order I am proposing in the first post.

The following code

ctx.WrapWithComponent("p");
ctx.WrapWithComponent("div");

To me clearly means 1: Wrap the component with p 2: Then wrap that with div

But is still a bit back to front, because you have to read the code from bottom to top again - but at least I expect it now.

<div>
  <p>
    <TestSubject/>
  </p>
</div>

Hmm my head doesn't work like that at all. That would we counter intuitive to me. To me, there is no direct relationship between the two calls that makes it obvious that the second ctx.WrapWithComponent("..") and refers to the first, and when we add add ctx.RenderComponent(..) into the mix, which must appear after the two calls, then the order makes less sense I think:

ctx.WrapWithComponent("p");
ctx.WrapWithComponent("div");
ctx.RenderComponent(<TestSubject/>)

Would become:

<div> // 2nd call
   <p> // 1st call
     <TestSubject/> // 3rd call
   </p>
</div>

Does that make more sense to you than:

ctx.ParentComponents.Add("div");
ctx.ParentComponents.Add("p");
ctx.RenderComponent(<TestSubject/>)

Becomes:

<div> // 1st call
   <p> // 2nd call
     <TestSubject/> // 3rd call
   </p>
</div>

How about something like HostingComponents?

ctx.HostingComponents.Add("p");
ctc.HostingComponents.Add("div");

To me this says 1: I want to add a p to host whatever comes after 2: Then I want to add a div inside that host, to host whatever comes after 3: Then I want my component inside the most recent host

Hosting instead of Parent. Have to think about it. What do you relate to the word Hosting?

mrpmorris commented 4 years ago

I have a test subject. If I do ParentComponents.Add("p") then I expect I have just parented my component in a p. But when I call ParentComponents.Add the second time it does it mean I want add something inside my last added parent, or does it again mean I want again to add a parent to what I have so far? It's just not unambiguous, you have to know, or try it.

I know the answer, but the question shouldn't be possible. It would be better to find a name that forces the answer to be "what else could it mean?"

I'm not happy with any suggestions so far, including my own.

mrpmorris commented 4 years ago

I think PageStructure.Add is pretty good. Structure says "things built in addition to other things" - in the HTML / component world the structure is

<ThisFirst>
  <ThisSecond>

And as components are ultimately hosted in pages, it makes sense that if you want to mock up a page to test a component within (how the user thinks they want to achieve their goal) then you'd look at PageStructure.

PageStructure.Add("A") PageStructure.Add("B")

Clearly means to create A, and then create B, because we are building in addition to an existing structure - rather than adding a parent.

Or you could go for RenderStructure.Add() if you want to avoid the word Page.

egil commented 4 years ago

I'm not happy with any suggestions so far, including my own.

Indeed. Naming is super hard, and is really important to get as unambiguous an API as possible. I appreciate all the input and viewpoints you provide.

Isn't structure just a more abstract way to talk about a render tree, e.g. a hierarchical nesting of components?

To me, this ...

PageStructure.Add("Foo")
PageStructure.Add("Bar")

... doesn't look a lot different than the previous examples. Some will probably not get the structure reference. I wonder if we need to make it more visual in the code, by looking at a completely different approach, i.e. something where the calls are nested in code as well.

But then again, the nesting based on ordering of calls is already in play bUnit when using the ChildContent() methods, e.g., to render:

<CascadingValue Value="foo" />
  <CascadingValue Value="bar" />
    <MyComp />

it can be done like this:

ctx.RenderComponent<MyComp >(
  CascadingValue(foo),
  CascadingValue(bar)
);

So it is like not a world ending thing if we cannot find a better way, since the chosen paradigm is discoverable.

I was thinking, maybe ParentComponents/CascadingValues could have a custom ToString() that returns the structure as a string :)

mrpmorris commented 4 years ago

The code is the same, but adding to a page structure suggests building on what you already have (like a new floor on a building), whereas adding a parent can also mean adding something before what you already have (in genealogy when you add a parent you don't change your own parent, you discover a new parent at the beginning).

So the second ParentComponents.Add doesn't mean Add, but InsertBetweenSubjectAndPreviousParent - which is an awful name :) Whereas PageStructure.Add can only mean to add to the structure you already have.

egil commented 4 years ago

Ahh, so the word Insert might be better compared to Add?

Also, what about RenderTree instead of both ParentComponents and CascadingValues:

E.g.:

ctx.RenderTree.InsertComponent<CompDIV>();
ctx.RenderTree.InsertComponent<CompP>();
ctx.RenderTree.InsertValue(new MyOtherValue());
ctx.RenderTree.InsertValue("named cascading value", new MyValue());
ctx.RenderComponent<MyComp>();

Would result in the following render tree:

<CompDIV>
  <CompP>
    <CascadingValue Value=@(new MyOtherValue())>
      <CascadingValue Name="named cascading value" Value=@(new MyValue())>
        <MyComp>
SQL-MisterMagoo commented 4 years ago

Are you sure you are not heading towards re-inventing the RenderTreeBuilder (RTB) with methods renamed for confusion? OpenComponent -> InsertComponent

It feels like that is the direction you are heading - at least in terms of a tree of components (from the viewpoint of someone watching the discussion evolve).

The RenderTreeBuilder is a familiar API for a Blazor developer (at least one who is writing C# bUnit tests) and as far as possible it feels like it might be more productive to mimic a known API rather than coming up with new names for the same concepts.

I can see that helper methods like InsertValue (for a CascadingValue) have merit as the raw RTB code is messy for that, but I wonder if sticking to the same API as RTB as much as possible would make it easier to work with?

egil commented 4 years ago

You might have a point @SQL-MisterMagoo, but as you say, the RTB API is messy, it's not meant for humans but for code generation, which is the reason I have different ways to pass parameters to components in bUnit in C# tests (e.g. see Passing Parameters to Components). The bUnit "RTB API" is not just for creating cascading values, but also for passing parameters and child content to components.

What do you think of having a property RenderTree on the TestContext, to which stuff can be added like cascading values and other components? Alternative name could RootRenderTree, CoreRenderTree, or BaseRenderTree, to make it more explicit what it is.

As for using the Open term vs Insert or Add, I am not sure. Open would lack its counter part Close, so I am more inclined to pick Add or Insert. Add has the advantage that people will recognize it from e.g. List<T> as something that appends something to the end of a collection, where as Insert is actually something that can be used to insert anywhere in the collection. That might be a good idea to include though, to enable controlling whether to insert at the top or bottom of the tree.

mrpmorris commented 4 years ago

Can you give the coder the ability to specify a renderfragment instead, and let them define where the component will go?

You could advise people to create these in one or more razor files and reference them in tests.

MyStatic = @<text>
  <p>@context</p>
<text>
egil commented 4 years ago

@mrpmorris is that RenderFragment syntax allowed in C# files?

Creating a RenderTree.Insert() overload that takes a RenderFragment as input isn't a problem, but if folks have to define it it in a .razor file, they might as well just create a component and insert that, or am I missing something?

chrissainty commented 4 years ago

I would agree with what @SQL-MisterMagoo mentions, I immediately thought the proposed APIs looked like a renamed RTB.

Have you considered a fluent style API?

ctx.RenderComponent<MyComp>()
   .WithCascadingValue(new MyOtherValue())
   .WithParent<ComP>()
   .WithParent<CompDiv>();

I quite like this style as for me it makes more sense, but I appreciate others aren't so keen on fluent APIs.

egil commented 4 years ago

I quite like this style as for me it makes more sense, but I appreciate others aren't so keen on fluent APIs.

@chrissainty I generally do too. My only problem in this case is that it would require a .Build() or something else to indicate to the test context that it can now start rendering, and that looks ugly in the basic case, e.g.:

// Fine in this case I guess, but still a little annoyed by the .Build()
ctx.RenderComponent<MyComp>()
   .WithCascadingValue(new MyOtherValue())
   .WithParent<ComP>()
   .WithParent<CompDiv>()
   .Build(); // or maybe Render()

// Sucks in this case...
ctx.RenderComponent<MyComp>().Build();

// compared to 
ctx.RenderComponent<MyComp>();

That said, there is a parameter builder available through the RenderComponent method, with all sorts of strongly typed magic to make sure that you cannot pass values to a parameter which doesnt match, e.g. for component:

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

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

this works really well I think:

ctx.RenderComponent<NonBlazorTypesParams>(parameters => parameters
  .Add(p => p.Numbers, 42)
  .Add(p => p.Lines, new List<string> { "Hello", "World" })
);

Anyway, back on topic, I think it is important to stress, that this feature is for things you would want to setup to make a test component render, e.g. calls to RenderTree.Add/ParentComponents.Add could easily be placed in "Setup" method, detached from the actual test method.

egil commented 4 years ago
ctx.RenderComponent<MyComp>()
   .WithCascadingValue(new MyOtherValue())
   .WithParent<ComP>()
   .WithParent<CompDiv>();

Btw. @chrissainty, example looks like this with the current parameter builder:

ctx.RenderComponent<CompDiv>(cdp => cdp
  .AddChildContent<ComP>(cpp => cpp
    .AddChildContent<MyComp>(mcp => mcp
      .Add(new MyOtherValue())
    )
  )
);

Assuming that CompDiv is the parent of ComP.

mrpmorris commented 4 years ago

The coder needn't call Build, you could have an overload that accepts a builder type and calls Build on it.

egil commented 4 years ago

The coder needn't call Build, you could have an overload that accepts a builder type and calls Build on it.

Hmm not following. Can you share an example?

mrpmorris commented 4 years ago
public static void Add(this List<IComponent> list, IBUnitComponentBuilder builder) 
{
  list.Add(builder.Build());
}
mrpmorris commented 4 years ago

I've just realised we've spent time discussing how we can, and not if we should.

What exactly is it we are trying to solve here? Why would, and how could, a component act differently when parented by another specific component?

chrissainty commented 4 years ago

What exactly is it we are trying to solve here? Why would, and how could, a component act differently when parented by another specific component?

UI controls, things like tabs or grids. If you're testing them as a whole then the parent component could be extremely important.

mrpmorris commented 4 years ago

I really can't think of an example scenario. Can you give me one?

chrissainty commented 4 years ago

Actually, the most simple example of this would probably be testing form components, they require the EditContext to be cascaded, that could be done by manually adding an EditContext as a cascading value, but an EditForm could just as easily be added. A more advanced scenario might be a grid of where its made up of lots of smaller components.

I'm not saying this feature would be extensively used, but I think it's useful for more advanced scenarios where testing component combinations matter. But then again, maybe I've missed the point of this feature :).

mrpmorris commented 4 years ago

Yes, we would use a cascading value to pass down an EditContext.

And that's the thing. At the moment there is no way for a component to know anything about its parent unless it is passed down via a cascading value. This means we never have to create a parent component in this way, we can create one in code pass it down as a cascading value.

Can you elaborate on your grid scenario? What exactly would the test be?

egil commented 4 years ago

OK folks, let me set the stage a little more precisely.

But first, this is not about rendering the component under test. Rendering a component with various parameters seems to work quite well with the "component parameter builder" or the "parameter factory methods" (examples here.

This is about registering prerequisites for tests. The three prerequisites that a component can have in Blazor seems to be services, parent components, and cascading values.

E.g. consider these two scenarios:

  1. Testing a custom component, e.g. <UserInfo>, which uses an <AuthorizeView> component. <AuthorizeView> requires a bunch of auth related services to be registered in the Services collection, and also a Task<AuthenticationState> cascading value. That value can be provided by the <CascadingAuthenticationState> component, as long as it is a parent of the custom <UserInfo> component.

If the user has multiple tests in the same test file for <UserInfo>, they would need to have code like this in every test: ctx.RenderComponent<CascadingAuthenticationState>(parameters => parameters.AddChildContent<UserInfo>()), which is unnecessary noise. Users should be able to say ctx.RenderComponent<UserInfo>(parameters => parameters ...).

Instead, I want to allow them to setup/register prerequisites for tests, that are not central to the test, e.g. say all components render in this test should be rendered inside the <CascadingAuthenticationState>. It's the same concept as with services, that are added to the Services collection, and are available to the components being rendered.

  1. Second case is related, but imaging a 3rd party component library that makes their components bUnit friendly, by having a ctx.AddVendorNameTestHelpers() in their library. This method registers any component library specific services and parent components required by the components in the component library. Then it becomes much easier for users to test their own components that use the 3rd party vendors component library.

Hope it makes sense.

mrpmorris commented 4 years ago

Surely this could be achieved by setting the expected cascading value, couldn't it?

egil commented 4 years ago

Surely this could be achieved by setting the expected cascading value, couldn't it?

Yes, but that is currently only supported through parameters to the RenderComponent method. I would like something separate to make the two scenarios described above possible.

mrpmorris commented 4 years ago

So you only need t add ctx.AddCascadingValue (if it doesn't already exist) and not a method for adding parent components, right?

egil commented 4 years ago

So you only need t add ctx.AddCascadingValue (if it doesn't already exist) and not a method for adding parent components, right?

No, I do not think it is enough. Sometimes it is easier or even necessary to add a parent component.

Thus, I am thinking a ctx.RootRenderTree property, with relevant Add methods, is a good solution. It mirrors the ctx.Services property with it's Add methods.

mrpmorris commented 4 years ago

I agree

egil commented 4 years ago

It hit me today that this conceptually is very similar to @layout in components, so that might actually be a good inspiration to the naming of things, e.g. ctx.AddLayout<TComponent>(component params).