Skip to content

Commit

Permalink
feat: ComponentFactories.Add<T>(instance)
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed Nov 23, 2021
1 parent 095e6a7 commit 0e396ef
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad

## [Unreleased]

### Added

- Add `ComponentFactories` extensions method that makes it easy to register an instance of a replacement component. By [@egil](https://github.com/egil).

### Fixed

- Changed `SetParametersAndRender` such that it rethrows any exceptions thrown by the component under tests `SetParametersAsync` method. Thanks to [@bonsall](https://github.com/bonsall) for reporting the issue. Fixed by [@egil](https://github.com/egil).
Expand Down
35 changes: 35 additions & 0 deletions src/bunit.core/ComponentFactories/InstanceComponentFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#if NET5_0_OR_GREATER
using System;
using Microsoft.AspNetCore.Components;

namespace Bunit.ComponentFactories
{
internal sealed class InstanceComponentFactory<TComponent> : IComponentFactory
where TComponent : IComponent
{
private readonly TComponent instance;
private int createCount;

public InstanceComponentFactory(TComponent instance)
=> this.instance = instance;

public bool CanCreate(Type componentType)
=> componentType == typeof(TComponent);

public IComponent Create(Type componentType)
{
if(createCount == 1)
{
throw new InvalidOperationException(
$"The instance object passed to the" +
$"{nameof(TestContextBase.ComponentFactories)}.{nameof(ComponentFactoryCollectionExtensions.Add)}<{typeof(TComponent).Name}>(instance) method can only be used to replace " +
$"one {typeof(TComponent)} component in the render tree.");
}

createCount++;

return instance;
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,33 @@ public static ComponentFactoryCollection Add<TComponent, TReplacementComponent>(
factories.Add(new GenericComponentFactory<TComponent, TReplacementComponent>());

return factories;
}
}

/// <summary>
/// Configures bUnit to replace a <typeparamref name="TComponent"/> component in the render tree
/// with the provided <paramref name="instance"/>.
/// </summary>
/// <remarks>
/// Only one <typeparamref name="TComponent"/> component can be replaced with the replacement component (<paramref name="instance"/>).
/// If there are two or more <typeparamref name="TComponent"/> components in the render tree, an exception is thrown.
/// </remarks>
/// <typeparam name="TComponent">Type of component to replace.</typeparam>
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
/// <param name="instance">The instance of the replacement component.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="factories"/> and/or <paramref name="instance"/> is null.</exception>
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
public static ComponentFactoryCollection Add<TComponent>(this ComponentFactoryCollection factories, TComponent instance)
where TComponent : IComponent
{
if (factories is null)
throw new ArgumentNullException(nameof(factories));
if (instance is null)
throw new ArgumentNullException(nameof(instance));

factories.Add(new InstanceComponentFactory<TComponent>(instance));

return factories;
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ namespace Bunit.ComponentFactories
{
public class GenericComponentFactoryTest : TestContext
{
[Fact(DisplayName = "UseFor throws when factories is null")]
[Fact(DisplayName = "Add throws when factories is null")]
public void Test001()
=> Should.Throw<ArgumentNullException>(() => ComponentFactoryCollectionExtensions.Add<Simple1, FakeSimple1>(factories: default));

[Fact(DisplayName = "UseFor<TComponent, TReplacementComponent> replaces components of type TComponent with TReplacementComponent")]
[Fact(DisplayName = "Add<TComponent, TReplacementComponent> replaces components of type TComponent with TReplacementComponent")]
public void Test002()
{
ComponentFactories.Add<Simple1, FakeSimple1>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#if NET5_0_OR_GREATER
using System;
using Bunit.TestAssets.SampleComponents;
using Moq;
using Shouldly;
using Xunit;

namespace Bunit.ComponentFactories
{
public class InstanceComponentFactoryTest : TestContext
{
[Fact(DisplayName = "Add throws when factories is null")]
public void Test001()
=> Should.Throw<ArgumentNullException>(() => ComponentFactoryCollectionExtensions.Add<Simple1>(default, default));

[Fact(DisplayName = "Add throws when instance is null")]
public void Test002()
=> Should.Throw<ArgumentNullException>(() => ComponentFactories.Add<Simple1>(default));

[Fact(DisplayName = "Factory replaces one TComponent with instance in the render tree")]
public void Test010()
{
var simple1Mock = new Mock<Simple1>();
ComponentFactories.Add<Simple1>(simple1Mock.Object);

var cut = RenderComponent<Wrapper>(ps => ps.AddChildContent<Simple1>());

cut.FindComponent<Simple1>().Instance
.ShouldBeSameAs(simple1Mock.Object);
}

[Fact(DisplayName = "Factory throws if component instance is requested twice for TComponent that inherits from ComponentBase")]
public void Test020()
{
var simple1Mock = new Mock<Simple1>();
ComponentFactories.Add<Simple1>(simple1Mock.Object);

Should.Throw<InvalidOperationException>(() => RenderComponent<TwoComponentWrapper>(ps => ps
.Add<Simple1>(p => p.First)
.Add<Simple1>(p => p.Second)));
}

[Fact(DisplayName = "Factory throws if component instance is requested twice for TComponent that implements from IComponent")]
public void Test021()
{
var simple1Mock = new Mock<BasicComponent>();
ComponentFactories.Add<BasicComponent>(simple1Mock.Object);

Should.Throw<InvalidOperationException>(() => RenderComponent<TwoComponentWrapper>(ps => ps
.Add<BasicComponent>(p => p.First)
.Add<BasicComponent>(p => p.Second)));
}
}
}
#endif
21 changes: 21 additions & 0 deletions tests/bunit.testassets/SampleComponents/BasicComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

namespace Bunit.TestAssets.SampleComponents
{
public class BasicComponent : IComponent
{
private RenderHandle renderHandle;

public void Attach(RenderHandle renderHandle)
{
this.renderHandle = renderHandle;
}

public Task SetParametersAsync(ParameterView parameters)
{
renderHandle.Render(builder => builder.AddMarkupContent(0, nameof(BasicComponent)));
return Task.CompletedTask;
}
}
}

0 comments on commit 0e396ef

Please sign in to comment.