Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test double components #400

Merged
merged 17 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 0 additions & 54 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -420,27 +420,6 @@ dotnet_diagnostic.CA1707.severity = error # https://github.com/atc-net
dotnet_diagnostic.CA2007.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA2007.md


# SecurityCodeScan
# https://security-code-scan.github.io/


# StyleCop
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
dotnet_diagnostic.SA1009.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1009.md
dotnet_diagnostic.SA1101.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1101.md
dotnet_diagnostic.SA1122.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1122.md
dotnet_diagnostic.SA1133.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1133.md
dotnet_diagnostic.SA1200.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1200.md
dotnet_diagnostic.SA1201.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1201.md
dotnet_diagnostic.SA1204.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1204.md
dotnet_diagnostic.SA1413.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1413.md
dotnet_diagnostic.SA1600.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1600.md
dotnet_diagnostic.SA1602.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1602.md
dotnet_diagnostic.SA1604.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1604.md
dotnet_diagnostic.SA1623.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1623.md
dotnet_diagnostic.SA1633.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1633.md


# SonarAnalyzer.CSharp
# https://rules.sonarsource.com/csharp
dotnet_diagnostic.S1135.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/SonarAnalyzerCSharp/S1135.md
Expand All @@ -467,31 +446,6 @@ dotnet_diagnostic.CA1031.severity = suggestion # CA1031: Do not catch general ex
dotnet_diagnostic.CA1032.severity = none # CA1032: Implement standard exception constructors
dotnet_diagnostic.CA2201.severity = suggestion # CA2201: Do not raise reserved exception types

# StyleCop
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
dotnet_diagnostic.SA1000.severity = none # BUGGY with C#9 code
dotnet_diagnostic.SA1005.severity = none # SA1005: Single line comments should begin with single space
dotnet_diagnostic.SA1011.severity = none # BUGGY with C#9 code

dotnet_diagnostic.SA1107.severity = suggestion # SA1107: Code should not contain multiple statements on one line
dotnet_diagnostic.SA1127.severity = suggestion # SA1127: Generic type constraints should be on their own line
dotnet_diagnostic.SA1128.severity = suggestion # SA1128: Put constructor initializers on their own line
dotnet_diagnostic.SA1133.severity = none # SA1133: Do not combine attributes
dotnet_diagnostic.SA1134.severity = none # SA1134: Attributes should not share line
dotnet_diagnostic.SA1202.severity = none # SA1202: Elements should be ordered by access
dotnet_diagnostic.SA1204.severity = none # SA1204: Static elements should appear before instance elements
dotnet_diagnostic.SA1201.severity = none # SA1201: Elements should appear in the correct order
dotnet_diagnostic.SA1404.severity = none # SA1404: Code analysis suppression should have justification
dotnet_diagnostic.SA1502.severity = none # SA1502: Element should not be on a single line
dotnet_diagnostic.SA1503.severity = none # SA1503: Braces should not be omitted
dotnet_diagnostic.SA1512.severity = none # SA1512: Single-line comments should not be followed by blank line
dotnet_diagnostic.SA1611.severity = silent # SA1611: Element parameters should be documented
dotnet_diagnostic.SA1515.severity = none # SA1515: Single-line comment should be preceded by blank line
dotnet_diagnostic.SA1516.severity = none # SA1516: Elements should be separated by blank line
dotnet_diagnostic.SA1615.severity = silent # SA1615: Element return value should be documented
dotnet_diagnostic.SA1618.severity = silent # SA1618: Generic type parameters should be documented
dotnet_diagnostic.SA1629.severity = suggestion # SA1629: Documentation text should end with a period

# SonarAnalyzer.CSharp
# https://rules.sonarsource.com/csharp
dotnet_diagnostic.S112.severity = none # S112: General exceptions should never be thrown
Expand Down Expand Up @@ -541,14 +495,6 @@ dotnet_diagnostic.CA1031.severity = none # CA1031: Do not catch general exceptio
dotnet_diagnostic.CA2012.severity = none # CA2012: Use ValueTasks correctly
dotnet_diagnostic.CA2201.severity = none # CA2201: Do not raise reserved exception types

# StyleCop
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
dotnet_diagnostic.SA0001.severity = none # SA0001: Xml Comment Analysis Disabled
dotnet_diagnostic.SA1107.severity = none # SA1107: Code should not contain multiple statements on one line
dotnet_diagnostic.SA1402.severity = none # SA1402: File may only contain a single type
dotnet_diagnostic.SA1601.severity = none # SA1601: Partial elements should be documented
dotnet_diagnostic.SA1649.severity = none # SA1649: File name should match first type name

# SonarAnalyzer.CSharp
# https://rules.sonarsource.com/csharp
dotnet_diagnostic.S125.severity = none # S125: Sections of code should not be commented out
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ List of added functionality in this release.

Learn more about this feature on the [Shallow rendering](https://bunit.dev/docs/test-doubles/shallow-rendering.html) page.

- Added `HasComponent<TComponent>()` to `IRenderedFragement`. Use it to check if the rendered fragment contains a component of type `TComponent`.

- Added `UseStubFor` and `UseDummyFor` extension methods to `ComponentFactories` that makes it easy to configure bUnit to replace components in the render tree with stubs and dummies (aka. test doubles). Both methods have overloads that allow for fine grained selection of component types to "double" during testing. Added by @Egil in [#400](https://github.com/bUnit-dev/bUnit/pull/400).

### Fixed

List of fixes in this release.
Expand Down
4 changes: 1 addition & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@
<ItemGroup Label="Code Analyzers">
<PackageReference Include="AsyncFixer" Version="1.5.1" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.661" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\stylecop.json" Visible="false" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.22.0.31243" PrivateAssets="All" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.23.0.32424" PrivateAssets="All" />
</ItemGroup>

<ItemGroup Condition="('$(TargetFramework)' == 'netstandard2.1' OR '$(TargetFramework)' == 'netcoreapp3.1') AND $(MSBuildProjectName) != 'bunit.template' AND $(MSBuildProjectName) != 'bunit'">
Expand Down
6 changes: 1 addition & 5 deletions src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,10 @@
# https://security-code-scan.github.io/


# StyleCop
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers


# SonarAnalyzer.CSharp
# https://rules.sonarsource.com/csharp


##########################################
# Custom - Code Analyzers Rules
##########################################
##########################################
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#if NET5_0_OR_GREATER
using System;
using Bunit.ComponentFactories;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;

namespace Bunit
{
/// <summary>
/// Extension methods for using component doubles.
/// </summary>
public static class ComponentFactoryCollectionExtensions
{
/// <summary>
/// Configures bUnit to use replace all components of type <typeparamref name="TComponent"/> (including derived components)
/// with a <see cref="Dummy{TComponent}"/> component in the render tree.
/// </summary>
/// <typeparam name="TComponent">The type of component to replace with a <see cref="Dummy{TComponent}"/> component.</typeparam>
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
public static ComponentFactoryCollection UseDummyFor<TComponent>(this ComponentFactoryCollection factories)
where TComponent : IComponent
{
return UseDummyFor(factories, CreatePredicate(typeof(TComponent)));

static Predicate<Type> CreatePredicate(Type componentTypeToStub)
=> componentType => componentType == componentTypeToStub || componentType.IsAssignableTo(componentTypeToStub);
}

/// <summary>
/// Configures bUnit to use replace all components whose type make the <paramref name="componentTypePredicate"/> predicate return <c>true</c>
/// with a <see cref="Dummy{TComponent}"/> component in the render tree.
/// </summary>
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
/// <param name="componentTypePredicate">The predicate which decides if a component should be replaced with a <see cref="Dummy{TComponent}"/> component.</param>
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
public static ComponentFactoryCollection UseDummyFor(this ComponentFactoryCollection factories, Predicate<Type> componentTypePredicate)
{
if (factories is null)
throw new ArgumentNullException(nameof(factories));
if (componentTypePredicate is null)
throw new ArgumentNullException(nameof(componentTypePredicate));

factories.Add(new DummyComponentFactory(componentTypePredicate));
return factories;
}

/// <summary>
/// Configures bUnit to use replace all components of type <typeparamref name="TComponent"/> (including derived components)
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
/// </summary>
/// <typeparam name="TComponent">The type of component to replace with a <see cref="Stub{TComponent}"/> component.</typeparam>
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
/// <param name="options">Optional options that configures how the <see cref="Stub{TComponent}"/> renders markup.</param>
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
public static ComponentFactoryCollection UseStubFor<TComponent>(this ComponentFactoryCollection factories, StubOptions? options = null)
where TComponent : IComponent
{
return UseStubFor(factories, CreatePredicate(typeof(TComponent)), options);

static Predicate<Type> CreatePredicate(Type componentTypeToStub)
=> componentType => componentType == componentTypeToStub || componentType.IsAssignableTo(componentTypeToStub);
}

/// <summary>
/// Configures bUnit to use replace all components whose type make the <paramref name="componentTypePredicate"/> predicate return <c>true</c>
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
/// </summary>
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
/// <param name="componentTypePredicate">The predicate which decides if a component should be replaced with a <see cref="Stub{TComponent}"/> component.</param>
/// <param name="options">Optional options that configures how the <see cref="Stub{TComponent}"/> renders markup.</param>
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
public static ComponentFactoryCollection UseStubFor(this ComponentFactoryCollection factories, Predicate<Type> componentTypePredicate, StubOptions? options = null)
{
if (factories is null)
throw new ArgumentNullException(nameof(factories));
if (componentTypePredicate is null)
throw new ArgumentNullException(nameof(componentTypePredicate));

factories.Add(new StubComponentFactory(componentTypePredicate, options ?? StubOptions.Default));
return factories;
}

/// <summary>
/// Configures bUnit to replace all components of type <typeparamref name="TComponent"/> with a component
/// of type <typeparamref name="TReplacementComponent"/>.
/// </summary>
/// <typeparam name="TComponent">Type of component to replace.</typeparam>
/// <typeparam name="TReplacementComponent">Type of component to replace with.</typeparam>
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
public static ComponentFactoryCollection UseFor<TComponent, TReplacementComponent>(this ComponentFactoryCollection factories)
where TComponent : IComponent
where TReplacementComponent : IComponent
{
if (factories is null)
throw new ArgumentNullException(nameof(factories));

factories.Add(new GenericComponentFactory<TComponent, TReplacementComponent>());

return factories;
}
}
}
#endif
22 changes: 22 additions & 0 deletions src/bunit.core/ComponentFactories/DummyComponentFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#if NET5_0_OR_GREATER
using System;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;

namespace Bunit.ComponentFactories
{
internal sealed class DummyComponentFactory : IComponentFactory
{
private readonly Predicate<Type> componentTypePredicate;

public DummyComponentFactory(Predicate<Type> componentTypePredicate)
=> this.componentTypePredicate = componentTypePredicate;

public bool CanCreate(Type componentType)
=> componentTypePredicate.Invoke(componentType);

public IComponent Create(Type componentType)
=> ComponentDoubleFactory.CreateDummy(componentType);
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

namespace Bunit.ComponentFactories
{
internal sealed class GenericComponentFactory<TComponent, TReplacementComponent> : IComponentFactory
where TComponent : IComponent
where TReplacementComponent : IComponent
{
public bool CanCreate(Type componentType) => componentType == typeof(TComponent);
public IComponent Create(Type componentType) => (IComponent)Activator.CreateInstance(typeof(TReplacementComponent))!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ namespace Bunit.ComponentFactories
/// all components using the <see cref="Stub{TComponent}"/>,
/// except the first child component of the <see cref="ShallowRenderContainer"/>.
/// </summary>
internal class ShallowRenderComponentFactory : IComponentFactory
internal sealed class ShallowRenderComponentFactory : IComponentFactory
{
private static readonly Type ShallowRenderContainerType = typeof(ShallowRenderContainer);
private static readonly StubOptions DefaultStubOptions = StubOptions.Default;
private bool shallowRenderContainerSeen;
private bool hasCreatedComponentUnderTest;

Expand All @@ -36,8 +37,7 @@ public IComponent Create(Type componentType)
{
if (hasCreatedComponentUnderTest)
{
var stubType = typeof(Stub<>).MakeGenericType(componentType);
return (IComponent)Activator.CreateInstance(stubType)!;
return ComponentDoubleFactory.CreateStub(componentType, DefaultStubOptions);
}

hasCreatedComponentUnderTest = true;
Expand Down
26 changes: 26 additions & 0 deletions src/bunit.core/ComponentFactories/StubComponentFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#if NET5_0_OR_GREATER
using System;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;

namespace Bunit.ComponentFactories
{
internal sealed class StubComponentFactory : IComponentFactory
{
private readonly Predicate<Type> componentTypePredicate;
private readonly StubOptions options;

public StubComponentFactory(Predicate<Type> componentTypePredicate, StubOptions options)
{
this.componentTypePredicate = componentTypePredicate;
this.options = options;
}

public bool CanCreate(Type componentType)
=> componentTypePredicate.Invoke(componentType);

public IComponent Create(Type componentType)
=> ComponentDoubleFactory.CreateStub(componentType, options);
}
}
#endif
38 changes: 38 additions & 0 deletions src/bunit.core/TestDoubles/Components/ComponentDoubleFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#if NET5_0_OR_GREATER
using System;
using Microsoft.AspNetCore.Components;

namespace Bunit.TestDoubles
{
internal static class ComponentDoubleFactory
{
private static readonly Type DummyType = typeof(Dummy<>);
private static readonly Type StubType = typeof(Stub<>);

/// <summary>
/// Create an instance of the <see cref="Dummy{TComponent}"/>, where <c>TComponent</c>
/// is the <paramref name="componentType"/>.
/// </summary>
/// <param name="componentType">The <c>TComponent</c> type.</param>
/// <returns>A instance of <see cref="Dummy{TComponent}"/>, where <c>TComponent</c> is <paramref name="componentType"/>.</returns>
public static IComponent CreateDummy(Type componentType)
{
var typeToCreate = DummyType.MakeGenericType(componentType);
return (IComponent)Activator.CreateInstance(typeToCreate)!;
}

/// <summary>
/// Create an instance of the <see cref="Stub{TComponent}"/>, where <c>TComponent</c>
/// is the <paramref name="componentType"/>.
/// </summary>
/// <param name="componentType">The <c>TComponent</c> type.</param>
/// <param name="stubOptions">Render options for the <see cref="Stub{TComponent}"/>.</param>
/// <returns>A instance of <see cref="Stub{TComponent}"/>, where <c>TComponent</c> is <paramref name="componentType"/>.</returns>
public static IComponent CreateStub(Type componentType, StubOptions stubOptions)
{
var typeToCreate = StubType.MakeGenericType(componentType);
return (IComponent)Activator.CreateInstance(typeToCreate, new object[] { stubOptions })!;
}
}
}
#endif
40 changes: 40 additions & 0 deletions src/bunit.core/TestDoubles/Components/Dummy{TComponent}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#if NET5_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

namespace Bunit.TestDoubles
{
/// <summary>
/// Represents a test double dummy of a component of type <typeparamref name="TComponent"/>.
/// </summary>
/// <typeparam name="TComponent">The stub type.</typeparam>
public sealed class Dummy<TComponent> : IComponent
where TComponent : IComponent
{
private readonly Type stubbedType = typeof(TComponent);

/// <summary>
/// Gets the parameters that was passed to the <typeparamref name="TComponent"/>
/// that this stub replaced in the component tree.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object> Parameters { get; private set; } = ImmutableDictionary<string, object>.Empty;

/// <inheritdoc/>
public override string ToString() => $"Dummy<{stubbedType.Name}>";

/// <inheritdoc/>
void IComponent.Attach(RenderHandle renderHandle) { }

/// <inheritdoc/>
Task IComponent.SetParametersAsync(ParameterView parameters)
{
Parameters = parameters.ToDictionary();
return Task.CompletedTask;
}
}
}
#endif
Loading