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

#115 - Adding placeholders for default services #223

Merged
merged 15 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from 8 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
13 changes: 13 additions & 0 deletions src/bunit.web/Extensions/TestServiceProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
using System;
using System.Net.Http;
using Bunit.Diffing;
using Bunit.Rendering;
using Bunit.TestDoubles.Authorization;
using Bunit.TestDoubles.HttpClient;
using Bunit.TestDoubles.JSInterop;
using Bunit.TestDoubles.Localization;
using Bunit.TestDoubles.Logging;
using Bunit.TestDoubles.NavigationManagement;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.JSInterop;
Expand All @@ -25,7 +33,12 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
services.AddSingleton<AuthenticationStateProvider, PlaceholderAuthenticationStateProvider>();
services.AddSingleton<IAuthorizationService, PlaceholderAuthorizationService>();
services.AddSingleton<IJSRuntime, PlaceholderJSRuntime>();
services.AddSingleton<NavigationManager, PlaceholderNavigationManager>();
services.AddSingleton<HtmlComparer>();
egil marked this conversation as resolved.
Show resolved Hide resolved
services.AddSingleton(new HttpClient(new PlaceholderHttpMessageHandler())
{BaseAddress = new Uri("http://localhost:5000")});
egil marked this conversation as resolved.
Show resolved Hide resolved
// services.AddSingleton<ILoggerFactory, PlaceholderLogFactory>();
services.AddSingleton<IStringLocalizer, PlaceholderStringLocalization>();
services.AddSingleton<BunitHtmlParser>();
services.AddSingleton<IRenderedComponentActivator, RenderedComponentActivator>();
return services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Net.Http;

namespace Bunit.TestDoubles.HttpClient
{
/// <summary>
/// Exception use to indicate that a mock HttpClient is required by a test
/// but was not provided.
/// </summary>
public class MissingMockHttpClientException : Exception
{
/// <summary>
/// The request that was sent via the http client
/// </summary>
public HttpRequestMessage Request { get; }

/// <summary>
/// Creates a new instance of the <see cref="MissingMockHttpClientException"/>
/// with the request that would have been handled
/// </summary>
/// <param name="request">The request being handled by the client</param>
public MissingMockHttpClientException(HttpRequestMessage request)
: base($"This test requires a HttpClient to be supplied, because the component under test invokes the HttpClient during the test. The request that was sent is contained within the '{nameof(Request)}' attribute of this exception. Guidance on mocking the HttpClient is available in the testing library's Wiki.")
egil marked this conversation as resolved.
Show resolved Hide resolved
{
Request = request;
}
egil marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Bunit.TestDoubles.HttpClient
egil marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// This MessageHandler for HttpClient is used to provide users with helpful
/// exceptions if they fail to provide a mock when required.
/// </summary>
public class PlaceholderHttpMessageHandler : HttpMessageHandler
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="MissingMockHttpClientException"></exception>
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new MissingMockHttpClientException(request);
}
}
}
egil marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Bunit.TestDoubles.Localization
{
/// <summary>
/// Exception use to indicate that a IStringLocalizer is required by a test
/// but was not provided.
/// </summary>
public class MissingMockStringLocalizationException : Exception
{

public object?[]? Arguments { get; }

/// <summary>
/// Creates a new instance of the <see cref="MissingMockStringLocalizationException"/>
/// with the method name and arguments used in the invocation
/// </summary>
/// <param name="methodName">The method that was called on the localizer</param>
/// <param name="arguments">The arguments that were passed in</param>
public MissingMockStringLocalizationException(string methodName, params object?[]? arguments)
:base($"This test requires a StringLocalizer to be supplied, because the component under test invokes the StringLocalizer during the test. The method that was called was '{methodName}', the parameters are container within the '{nameof(Arguments)}' property of this exception. Guidance on mocking the StringLocalizer is available in the testing library's Wiki.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:base($"This test requires a StringLocalizer to be supplied, because the component under test invokes the StringLocalizer during the test. The method that was called was '{methodName}', the parameters are container within the '{nameof(Arguments)}' property of this exception. Guidance on mocking the StringLocalizer is available in the testing library's Wiki.")
:base($"This test requires a IStringLocalizer to be supplied, because the component under test invokes the IStringLocalizer during the test. The method that was called was '{methodName}', the parameters are container within the '{nameof(Arguments)}' property of this exception.")

Lets reference the interface in the error message instead, and remove the reference to guidance on the website until there is one :)

{
Arguments = arguments;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Globalization;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;

namespace Bunit.TestDoubles.Localization
{
/// <summary>
/// This IStringLocalizer is used to provide users with helpful exceptions if they fail to provide a mock when required.
/// </summary>
public class PlaceholderStringLocalization : IStringLocalizer
{
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use expression membered bodies here (=>).

throw new MissingMockStringLocalizationException(nameof(GetAllStrings), includeParentCultures);
}

public IStringLocalizer WithCulture(CultureInfo culture)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use expression membered bodies here (=>).

throw new MissingMockStringLocalizationException(nameof(WithCulture), culture);
}

public LocalizedString this[string name]
=> throw new MissingMockStringLocalizationException("GetByIndex", name);

public LocalizedString this[string name, params object[] arguments]
=> throw new MissingMockStringLocalizationException("GetByIndex", name, arguments);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

namespace Bunit.TestDoubles.Logging
{
/// <summary>
/// Exception use to indicate that a Logger is required by a test
/// but was not provided.
/// </summary>
public class MissingMockLoggerFactoryException : Exception
{
/// <summary>
/// The method that was called by the logger factory
/// </summary>
public string MethodName { get; }

/// <summary>
/// The parameters passed into the logger factory
/// </summary>
public object?[]? Arguments { get; }

/// <summary>
/// Creates a new instance of <see cref="MissingMockLoggerFactoryException"/>
/// with the arguments used in the invocation.
/// </summary>
/// <param name="methodName"></param>
/// <param name="arguments"></param>
public MissingMockLoggerFactoryException(string methodName, params object?[]? arguments)
: base ($"This test requires a LoggerFactory to be supplied, because the component under test invokes the LoggerFactory during the test. The method invocation was '{methodName}', the arguments are contained within the '{nameof(Arguments)}' attribute of this exception. Guidance on mocking the LoggerFactory is available in the testing library's Wiki.")
{
MethodName = methodName;
Arguments = arguments;
}
}
}
24 changes: 24 additions & 0 deletions src/bunit.web/TestDoubles/Logging/PlaceholderLogFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;

namespace Bunit.TestDoubles.Logging
{
/// <summary>
/// This LogFactory is used to provide users with helpful exceptions if they fail to provide a mock when required.
/// </summary>
public class PlaceholderLogFactory : ILoggerFactory
{
public void Dispose()
{
}

public ILogger CreateLogger(string categoryName)
{
throw new MissingMockLoggerFactoryException(nameof(CreateLogger), categoryName);
}

public void AddProvider(ILoggerProvider provider)
{
throw new MissingMockLoggerFactoryException(nameof(AddProvider), provider);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

namespace Bunit.TestDoubles.NavigationManagement
{
/// <summary>
/// Exception use to indicate that a NavigationManager is required by a test
/// but was not provided.
/// </summary>
public class MissingMockNavigationManagerException : Exception
{
/// <summary>
/// Creates a new instance of the <see cref="MissingMockNavigationManagerException"/>
/// with the arguments used in the invocation.
/// </summary>
/// <param name="uri">Uri to navigate to</param>
/// <param name="forceLoad">Whether to force load</param>
public MissingMockNavigationManagerException(string uri, bool forceLoad)
: base ($"This test requires a NavigationManager to be supplied, because the component under test invokes the NavigationManager during the test. The url that was requested was '{uri}' with a force reload value of '{forceLoad.ToString()}'. Guidance on mocking the NavigationManager is available in the testing library's Wiki.")
{

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Components;

namespace Bunit.TestDoubles.NavigationManagement
{
/// <summary>
/// This NavigationManager is used to provide users with helpful exceptions if they fail to provide a mock when required.
/// </summary>
public class PlaceholderNavigationManager : NavigationManager
{
protected override void NavigateToCore(string uri, bool forceLoad)
{
throw new MissingMockNavigationManagerException(uri, forceLoad);
}

protected override void EnsureInitialized()
{
Initialize("http://localhost:5000/", "http://localhost:5000/");
egil marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
1 change: 1 addition & 0 deletions src/bunit.web/bunit.web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageReference Include="AngleSharp.Css" Version="0.14.0" />
<PackageReference Include="AngleSharp.Diffing" Version="0.14.0" />
<PackageReference Include="AngleSharp.Wrappers" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.8" />
</ItemGroup>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.8" />
</ItemGroup>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="5.0.0-rc.1.*" />
</ItemGroup>

We have to target both .net5 and netstandard2.1

<ItemGroup>
Expand Down
10 changes: 10 additions & 0 deletions tests/bunit.testassets/SampleComponents/SimpleNavigation.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@inject NavigationManager Navigation

<h3>SimpleNavigation</h3>

@code {
protected override void OnInitialized()
{
Navigation.NavigateTo("Index");
}
}
12 changes: 12 additions & 0 deletions tests/bunit.testassets/SampleComponents/SimpleUsingLocalizer.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@using Microsoft.Extensions.Localization
@inject IStringLocalizer StringLocalizer

<h3>SimpleUsingLocalizer</h3>

@code {
protected override void OnInitialized()
{
var localizedString = StringLocalizer["StringName"];
}

}
12 changes: 12 additions & 0 deletions tests/bunit.testassets/SampleComponents/SimpleWithHttpClient.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@inject HttpClient HttpClient


<h3>SimpleWithHttpClient</h3>

@code {
protected override async Task OnInitializedAsync()
{
await HttpClient.GetAsync("/api/weather");
StateHasChanged();
}
}
12 changes: 12 additions & 0 deletions tests/bunit.testassets/SampleComponents/SimpleWithLogger.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@using Microsoft.Extensions.Logging
@inject ILoggerFactory LoggerFactory

<h3>SimpleWithLogger</h3>

@code {
protected override void OnInitialized()
{
var logger = LoggerFactory.CreateLogger<SimpleWithLogger>();
logger.LogDebug(nameof(OnInitialized));
}
}
egil marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions tests/bunit.testassets/bunit.testassets.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.8" />
egil marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Microsoft.AspNetCore.Components" Version="5.0.0-rc.1.*" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.0-rc.1.*" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.0-rc.1.*" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="5.0.0-rc.1.*" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions tests/bunit.web.tests/TestContextTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using Bunit.TestAssets.SampleComponents;
using Bunit.TestDoubles.HttpClient;
using Bunit.TestDoubles.JSInterop;
using Bunit.TestDoubles.Localization;
using Bunit.TestDoubles.Logging;
using Bunit.TestDoubles.NavigationManagement;
using Shouldly;
using Xunit;

Expand All @@ -22,5 +26,29 @@ public void Test022()

RenderComponent<SimpleWithJSRuntimeDep>();
}

[Fact(DisplayName = "The test service provider should register a placeholder NavigationManager which throws exceptions")]
public void Test023()
{
Should.Throw<MissingMockNavigationManagerException>(() => RenderComponent<SimpleNavigation>());
}

[Fact(DisplayName = "The test service provider should register a placeholder HttpClient which throws exceptions")]
public void Test024()
{
Should.Throw<MissingMockHttpClientException>(() => RenderComponent<SimpleWithHttpClient>());
}

[Fact(DisplayName = "The test service provider should register a placeholder LoggerFactory which throws exceptions")]
public void Test025()
{
// Should.Throw<MissingMockLoggerFactoryException>(() => RenderComponent<SimpleWithLogger>());
}
egil marked this conversation as resolved.
Show resolved Hide resolved

[Fact(DisplayName = "The test service provider should register a placeholder IStringLocalizer which throws exceptions")]
public void Test026()
{
Should.Throw<MissingMockStringLocalizationException>(() => RenderComponent<SimpleUsingLocalizer>());
}
}
}