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 9 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
20 changes: 20 additions & 0 deletions src/bunit.web/Extensions/TestServiceProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
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.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 @@ -16,16 +23,29 @@ namespace Bunit.Extensions
/// </summary>
public static class TestServiceProviderExtensions
{
// Have to make these fields so that the compiler thinks we will dispose of them
// later
private static HttpClient? _implementationInstance;
private static PlaceholderHttpMessageHandler? _placeholderHttpMessageHandler;

/// <summary>
/// Registers the default services required by the web <see cref="TestContext"/>.
/// </summary>
public static IServiceCollection AddDefaultTestContextServices(this IServiceCollection services)
{
_placeholderHttpMessageHandler = new PlaceholderHttpMessageHandler();
_implementationInstance = new HttpClient(_placeholderHttpMessageHandler)
{BaseAddress = new Uri("http://localhost:5000")};

services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
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(_implementationInstance);
// 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,30 @@
using System;
using System.Collections.Generic;

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
{

/// <summary>
/// The arguments that were passed into the localizer
/// </summary>
public IReadOnlyList<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 ?? Array.Empty<object?>();;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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
{
/// <summary>
/// Will throw exception to prompt the user
/// </summary>
/// <param name="includeParentCultures"></param>
/// <returns></returns>
/// <exception cref="MissingMockStringLocalizationException"></exception>
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);
}

/// <summary>
/// Will throw exception to prompt the user
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
/// <exception cref="MissingMockStringLocalizationException"></exception>
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);
}

/// <summary>
/// Will throw exception to prompt the user
/// </summary>
/// <param name="name"></param>
public LocalizedString this[string name]
=> Throw(name);

/// <summary>
/// Will throw exception to prompt the user
/// </summary>
/// <param name="name"></param>
/// <param name="arguments"></param>
public LocalizedString this[string name, params object[] arguments]
=> Throw(name, arguments);

private static LocalizedString Throw(string name, params object?[]? args)
{
throw new MissingMockStringLocalizationException("GetByIndex", name, args);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;

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 IReadOnlyList<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 ?? Array.Empty<object?>();
}
}
}
50 changes: 50 additions & 0 deletions src/bunit.web/TestDoubles/Logging/PlaceholderLogFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
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
{
/// <summary>
///
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);

}

/// <summary>
///
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
}

/// <summary>
///
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
/// <exception cref="MissingMockLoggerFactoryException"></exception>
public ILogger CreateLogger(string categoryName)
{
throw new MissingMockLoggerFactoryException(nameof(CreateLogger), categoryName);
}

/// <summary>
///
/// </summary>
/// <param name="provider"></param>
/// <exception cref="MissingMockLoggerFactoryException"></exception>
public void AddProvider(ILoggerProvider provider)
{
throw new MissingMockLoggerFactoryException(nameof(AddProvider), provider);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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="url">Uri to navigate to</param>
/// <param name="forceLoad">Whether to force load</param>
public MissingMockNavigationManagerException(string url, 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 '{url}' with a force reload value of '{forceLoad.ToString()}'. Guidance on mocking the NavigationManager is available in the testing library's Wiki.")
egil marked this conversation as resolved.
Show resolved Hide resolved
{

}

/// <summary>
/// Creates a new instance of the <see cref="MissingMockNavigationManagerException"/>
/// with the arguments used in the invocation.
/// </summary>
/// <param name="url">Uri to navigate to</param>
/// <param name="forceLoad">Whether to force load</param>
public MissingMockNavigationManagerException(Uri url, 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 '{url}' 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,30 @@
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
{
/// <summary>
/// Will throw exception to prompt user
/// </summary>
/// <param name="uri"></param>
/// <param name="forceLoad"></param>
/// <exception cref="MissingMockNavigationManagerException"></exception>
protected override void NavigateToCore(string uri, bool forceLoad)
{
throw new MissingMockNavigationManagerException(uri, forceLoad);
}

/// <summary>
/// Will initialize the navigation manager with a hard coded
/// value of http://localhost:5000/
/// </summary>
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();
}
}
Loading