[Fixes #6233] Productionize and harden our functional testing infrastructure

This commit is contained in:
Javier Calvarro Nelson 2017-06-22 11:11:27 -07:00
parent aa5a348385
commit 35152d5933
19 changed files with 770 additions and 97 deletions

35
Mvc.sln
View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.9
VisualStudioVersion = 15.0.26615.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -122,6 +122,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorPagesWebSite", "test\W
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Performance", "test\Microsoft.AspNetCore.Mvc.Performance\Microsoft.AspNetCore.Mvc.Performance.csproj", "{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Testing", "src\Microsoft.AspNetCore.Mvc.Testing\Microsoft.AspNetCore.Mvc.Testing.csproj", "{7500B228-1769-4CFB-A571-3DFAC6678A06}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Mvc.Testing.Xunit", "src\Microsoft.AspNetCore.Mvc.Testing.Xunit\Microsoft.AspNetCore.Mvc.Testing.Xunit.csproj", "{5248D809-E5E5-49FE-B3E8-428D454C63B3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -756,6 +760,30 @@ Global
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}.Release|x86.ActiveCfg = Release|Any CPU
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}.Release|x86.Build.0 = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|x86.ActiveCfg = Debug|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|x86.Build.0 = Debug|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Any CPU.Build.0 = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|x86.ActiveCfg = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|x86.Build.0 = Release|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|x86.ActiveCfg = Debug|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|x86.Build.0 = Debug|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Any CPU.Build.0 = Release|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|x86.ActiveCfg = Release|Any CPU
{5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -818,5 +846,10 @@ Global
{0AB46520-F441-4E01-B444-08F4D23F8B1B} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{4BA6EC9A-B6D9-41F2-BFDA-D82B22D80352} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{7500B228-1769-4CFB-A571-3DFAC6678A06} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{5248D809-E5E5-49FE-B3E8-428D454C63B3} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,82 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Threading;
namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
{
internal class CultureReplacer : IDisposable
{
private const string _defaultCultureName = "en-GB";
private const string _defaultUICultureName = "en-US";
private static readonly CultureInfo _defaultCulture = new CultureInfo(_defaultCultureName);
private readonly CultureInfo _originalCulture;
private readonly CultureInfo _originalUICulture;
private readonly long _threadId;
// Culture => Formatting of dates/times/money/etc, defaults to en-GB because en-US is the same as InvariantCulture
// We want to be able to find issues where the InvariantCulture is used, but a specific culture should be.
//
// UICulture => Language
public CultureReplacer(string culture = _defaultCultureName, string uiCulture = _defaultUICultureName)
: this(new CultureInfo(culture), new CultureInfo(uiCulture))
{
}
public CultureReplacer(CultureInfo culture, CultureInfo uiCulture)
{
_originalCulture = CultureInfo.CurrentCulture;
_originalUICulture = CultureInfo.CurrentUICulture;
_threadId = Thread.CurrentThread.ManagedThreadId;
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = uiCulture;
}
/// <summary>
/// The name of the culture that is used as the default value for CultureInfo.DefaultThreadCurrentCulture when CultureReplacer is used.
/// </summary>
public static string DefaultCultureName
{
get { return _defaultCultureName; }
}
/// <summary>
/// The name of the culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentUICulture when CultureReplacer is used.
/// </summary>
public static string DefaultUICultureName
{
get { return _defaultUICultureName; }
}
/// <summary>
/// The culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentCulture when CultureReplacer is used.
/// </summary>
public static CultureInfo DefaultCulture
{
get { return _defaultCulture; }
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
if(Thread.CurrentThread.ManagedThreadId != _threadId)
{
throw new InvalidOperationException("The current thread is not the same as the thread " +
"invoking the constructor. This should never happen.");
}
CultureInfo.CurrentCulture = _originalCulture;
CultureInfo.CurrentUICulture = _originalUICulture;
}
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
{
/// <summary>
/// A middleware that ensures web sites run in a consistent culture. Currently useful for tests that format dates,
/// times, or numbers. Will be more useful when we have localized resources.
/// </summary>
public class CultureReplacerMiddleware
{
// Have no current need to use cultures other than the ReplaceCultureAttribute defaults (en-GB, en-US).
private readonly ReplaceCultureAttribute _replaceCulture = new ReplaceCultureAttribute();
private readonly RequestDelegate _next;
public CultureReplacerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Use ReplaceCultureAttribute to avoid thread consistency checks in CultureReplacer. await doesn't
// necessarily end on the original thread. For this case, problems arise when next middleware throws. Can
// remove the thread consistency checks once culture is (at least for .NET 4.6) handled using
// AsyncLocal<CultureInfo>.
try
{
_replaceCulture.Before(methodUnderTest: null);
await _next(context);
}
finally
{
_replaceCulture.After(methodUnderTest: null);
}
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
{
/// <summary>
/// Inserts the <see cref="CultureReplacerMiddleware"/> at the beginning of the pipeline.
/// </summary>
public class CultureReplacerStartupFilter : IStartupFilter
{
/// <inheritdoc />
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return AddCulture;
void AddCulture(IApplicationBuilder builder)
{
builder.UseMiddleware<CultureReplacerMiddleware>();
next(builder);
}
}
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Reflection;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
{
/// <summary>
/// Replaces the current culture and UI culture for the test.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
internal class ReplaceCultureAttribute : BeforeAfterTestAttribute
{
private const string _defaultCultureName = "en-GB";
private const string _defaultUICultureName = "en-US";
private CultureInfo _originalCulture;
private CultureInfo _originalUICulture;
/// <summary>
/// Replaces the current culture and UI culture to en-GB and en-US respectively.
/// </summary>
public ReplaceCultureAttribute() :
this(_defaultCultureName, _defaultUICultureName)
{
}
/// <summary>
/// Replaces the current culture and UI culture based on specified values.
/// </summary>
public ReplaceCultureAttribute(string currentCulture, string currentUICulture)
{
Culture = new CultureInfo(currentCulture);
UICulture = new CultureInfo(currentUICulture);
}
/// <summary>
/// The <see cref="CultureInfo.CurrentCulture"/> for the test. Defaults to en-GB.
/// </summary>
/// <remarks>
/// en-GB is used here as the default because en-US is equivalent to the InvariantCulture. We
/// want to be able to find bugs where we're accidentally relying on the Invariant instead of the
/// user's culture.
/// </remarks>
public CultureInfo Culture { get; }
/// <summary>
/// The <see cref="CultureInfo.CurrentUICulture"/> for the test. Defaults to en-US.
/// </summary>
public CultureInfo UICulture { get; }
public override void Before(MethodInfo methodUnderTest)
{
_originalCulture = CultureInfo.CurrentCulture;
_originalUICulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentCulture = Culture;
CultureInfo.CurrentUICulture = UICulture;
}
public override void After(MethodInfo methodUnderTest)
{
CultureInfo.CurrentCulture = _originalCulture;
CultureInfo.CurrentUICulture = _originalUICulture;
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<Description>XUnit convenience fixture for creating functional tests for MVC applications.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageTags>aspnetcore;aspnetcoremvc;aspnetcoremvctesting</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="$(XunitVersion)" />
</ItemGroup>
<ItemGroup>
<Content Include="build\**\*.targets" Pack="true" PackagePath="%(Identity)" />
<Content Include="build\xunit.runner.json" Pack="true" PackagePath="%(Identity)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Testing\Microsoft.AspNetCore.Mvc.Testing.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,126 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
/// <summary>
/// XUnit fixture for bootstrapping an application in memory for functional end to end tests.
/// </summary>
/// <typeparam name="TStartup">The applications startup class.</typeparam>
public class WebApplicationTestFixture<TStartup> : IDisposable where TStartup : class
{
private readonly TestServer _server;
public WebApplicationTestFixture()
: this("src")
{
}
protected WebApplicationTestFixture(string solutionRelativePath)
: this("*.sln", solutionRelativePath)
{
}
protected WebApplicationTestFixture(string solutionSearchPattern, string solutionRelativePath)
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
// This step assumes project name = assembly name.
var projectName = startupAssembly.GetName().Name;
var projectPath = Path.Combine(solutionRelativePath, projectName);
var builder = new MvcWebApplicationBuilder<TStartup>()
.UseSolutionRelativeContentRoot(projectPath)
.UseApplicationAssemblies();
ConfigureApplication(builder);
var xunitRunnerJson = new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), "xunit.runner.json"));
if (!xunitRunnerJson.Exists)
{
Console.WriteLine("Can't find xunit.runner.json. " +
"Functional tests require '\"shadowCopy\": false' to work properly. " +
"Make sure your XUnit configuration has that setup.");
}
var content = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(xunitRunnerJson.FullName));
if (!content.TryGetValue("shadowCopy", out var token) || !(bool)token)
{
Console.WriteLine("'shadowCopy' is not set to true on xunit.runner.json. " +
"Functional tests require '\"shadowCopy\": false' to work properly. " +
"Make sure your XUnit configuration has that setup.");
}
using (new CultureReplacer())
{
_server = builder.Build();
}
Client = _server.CreateClient();
Client.BaseAddress = new Uri("http://localhost");
}
/// <summary>
/// Gives a fixture an opportunity to configure the application before it gets built.
/// </summary>
/// <param name="builder">The <see cref="MvcWebApplicationBuilder{TStartup}"/> for the application.</param>
protected virtual void ConfigureApplication(MvcWebApplicationBuilder<TStartup> builder)
{
builder.ConfigureAfterStartup(s => s.TryAddEnumerable(ServiceDescriptor.Transient<IStartupFilter, CultureReplacerStartupFilter>()));
}
public HttpClient Client { get; }
public HttpClient CreateClient()
{
var client = _server.CreateClient();
client.BaseAddress = new Uri("http://localhost");
return client;
}
public HttpClient CreateClient(Uri baseAddress, params DelegatingHandler[] handlers)
{
if (handlers.Length == 0)
{
var client = _server.CreateClient();
client.BaseAddress = baseAddress;
return client;
}
else
{
for (var i = handlers.Length - 1; i > 1; i++)
{
handlers[i - 1].InnerHandler = handlers[i];
}
var serverHandler = _server.CreateHandler();
handlers[handlers.Length - 1].InnerHandler = serverHandler;
var client = new HttpClient(handlers[0]);
client.BaseAddress = baseAddress;
return client;
}
}
public void Dispose()
{
Client.Dispose();
_server.Dispose();
}
}
}

View File

@ -0,0 +1,14 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CopyXunitRunner" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
<ItemGroup>
<XunitRunnerJson Include="$(MSBuildThisFileDirectory)xunit.runner.json" />
</ItemGroup>
<PropertyGroup>
<ShouldCopyXUnitRunnerJson Condition="Exists('%(XunitRunnerJson.FullPath)') and !Exists('$(OutputPath)xunit.runner.json') and '$(DisableCopyXunitRunnerJson)' == ''" >true</ShouldCopyXUnitRunnerJson>
</PropertyGroup>
<Copy SourceFiles="%(XunitRunnerJson.FullPath)" DestinationFolder="$(OutputPath)" Condition="'$(ShouldCopyXUnitRunnerJson)' != ''" />
<Message Condition="'$(ShouldCopyXUnitRunnerJson)' != ''" Text="Automatically copied 'xunit.runner.json' to '$(OutputPath)'. Set a non-empty value to 'DisableCopyXunitRunnerJson' to disable this behavior. You will have to manually configure 'shadowCopy: false' for functional tests to run properly." />
</Target>
</Project>

View File

@ -0,0 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Testing.Internal
{
/// <summary>
/// Delegating handler for managing cookies on functional tests.
/// </summary>
public class CookieContainerHandler : DelegatingHandler
{
public CookieContainerHandler(HttpMessageHandler innerHandler)
: base(innerHandler)
{
}
public CookieContainer Container { get; } = new CookieContainer();
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var cookieHeader = Container.GetCookieHeader(request.RequestUri);
request.Headers.Add("Cookie", cookieHeader);
var response = await base.SendAsync(request, cancellationToken);
if (response.Headers.TryGetValues("Set-Cookie", out var setCookieHeaders))
{
foreach (var header in setCookieHeaders)
{
Container.SetCookies(response.RequestMessage.RequestUri, header);
}
}
return response;
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Testing.Internal
{
/// <summary>
/// Helper class to orchestrate service registrations in <see cref="TestStartup{TStartup}"/>.
/// </summary>
public class TestServiceRegistrations
{
public IList<Action<IServiceCollection>> Before { get; set; } = new List<Action<IServiceCollection>>();
public IList<Action<IServiceCollection>> After { get; set; } = new List<Action<IServiceCollection>>();
public void ConfigureServices(IServiceCollection services, Action startupConfigureServices)
{
foreach (var config in Before)
{
config(services);
}
startupConfigureServices();
foreach (var config in After)
{
config(services);
}
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Testing.Internal
{
/// <summary>
/// Fake startup class used in functional tests to decorate the registration of
/// ConfigureServices.
/// </summary>
/// <typeparam name="TStartup">The startup class of your application.</typeparam>
public class TestStartup<TStartup> where TStartup : class
{
private readonly IServiceProvider _serviceProvider;
private readonly TestServiceRegistrations _registrations;
private readonly TStartup _instance;
public TestStartup(IServiceProvider serviceProvider, TestServiceRegistrations registrations)
{
_serviceProvider = serviceProvider;
_registrations = registrations;
_instance = (TStartup)ActivatorUtilities.CreateInstance(serviceProvider, typeof(TStartup));
}
public void ConfigureServices(IServiceCollection services)
{
var configureServices = _instance.GetType().GetMethod(nameof(ConfigureServices));
var parameters = Enumerable.Repeat(services, 1)
.Concat(configureServices
.GetParameters()
.Skip(1)
.Select(p => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, p.ParameterType)))
.ToArray();
_registrations.ConfigureServices(services, () => configureServices.Invoke(_instance, parameters));
}
public void Configure(IApplicationBuilder applicationBuilder)
{
var configure = _instance.GetType().GetMethod(nameof(Configure));
var parameters = Enumerable.Repeat(applicationBuilder, 1)
.Concat(configure
.GetParameters()
.Skip(1)
.Select(p => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, p.ParameterType)))
.ToArray();
configure.Invoke(_instance, parameters);
}
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<Description>Support for writing functional tests for MVC applications.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageTags>aspnetcore;aspnetcoremvc;aspnetcoremvctesting</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(AspNetCoreVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="build\**\*.targets" Pack="true" PackagePath="%(Identity)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,136 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Testing.Internal;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Testing
{
/// <summary>
/// Builder API for bootstraping an MVC application for functional tests.
/// </summary>
/// <typeparam name="TStartup">The application startup class.</typeparam>
public class MvcWebApplicationBuilder<TStartup> where TStartup : class
{
public string ContentRoot { get; set; }
public IList<Action<IServiceCollection>> ConfigureServicesBeforeStartup { get; set; } = new List<Action<IServiceCollection>>();
public IList<Action<IServiceCollection>> ConfigureServicesAfterStartup { get; set; } = new List<Action<IServiceCollection>>();
public List<Assembly> ApplicationAssemblies { get; set; } = new List<Assembly>();
/// <summary>
/// Configures services before <see cref="TStartup.ConfigureServices"/> runs.
/// </summary>
/// <param name="configure">The <see cref="Action{IServiceCollection}"/> to configure the services with.</param>
/// <returns>An instance of this <see cref="MvcWebApplicationBuilder{TStartup}"/></returns>
public MvcWebApplicationBuilder<TStartup> ConfigureBeforeStartup(Action<IServiceCollection> configure)
{
ConfigureServicesBeforeStartup.Add(configure);
return this;
}
/// <summary>
/// Configures services after <see cref="TStartup.ConfigureServices"/> runs.
/// </summary>
/// <param name="configure">The <see cref="Action{IServiceCollection}"/> to configure the services with.</param>
/// <returns>An instance of this <see cref="MvcWebApplicationBuilder{TStartup}"/></returns>
public MvcWebApplicationBuilder<TStartup> ConfigureAfterStartup(Action<IServiceCollection> configure)
{
ConfigureServicesAfterStartup.Add(configure);
return this;
}
/// <summary>
/// Configures <see cref="ApplicationPartManager"/> to include the default set
/// of <see cref="ApplicationPart"/> provided by <see cref="DefaultAssemblyPartDiscoveryProvider"/>.
/// </summary>
/// <returns>An instance of this <see cref="MvcWebApplicationBuilder{TStartup}"/></returns>
public MvcWebApplicationBuilder<TStartup> UseApplicationAssemblies()
{
var depsFileName = $"{typeof(TStartup).Assembly.GetName().Name}.deps.json";
var depsFile = new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), depsFileName));
if (!depsFile.Exists)
{
throw new InvalidOperationException($"Can't find'{depsFile}'. This file is required for functional tests " +
$"to run properly. There should be a copy of the file on your source project bin folder. If thats not the " +
$"case, make sure that the property PreserveCompilationContext is set to true on your project file. E.g" +
$"'<PreserveCompilationContext>true</PreserveCompilationContext>'.");
}
ApplicationAssemblies.AddRange(DefaultAssemblyPartDiscoveryProvider
.DiscoverAssemblyParts(typeof(TStartup).Assembly.GetName().Name)
.Select(s => ((AssemblyPart)s).Assembly)
.ToList());
return this;
}
/// <summary>
/// Configures the application content root.
/// </summary>
/// <param name="solutionName">The glob pattern to use for finding the solution.</param>
/// <param name="solutionRelativePath">The relative path to the content root from the solution file.</param>
/// <returns>An instance of this <see cref="MvcWebApplicationBuilder{TStartup}"/></returns>
public MvcWebApplicationBuilder<TStartup> UseSolutionRelativeContentRoot(
string solutionRelativePath,
string solutionName = "*.sln")
{
var applicationBasePath = AppContext.BaseDirectory;
var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault();
if (solutionPath != null)
{
ContentRoot = Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath));
return this;
}
directoryInfo = directoryInfo.Parent;
}
while (directoryInfo.Parent != null);
throw new Exception($"Solution root could not be located using application root {applicationBasePath}.");
}
public TestServer Build()
{
var builder = new WebHostBuilder()
.UseStartup<TestStartup<TStartup>>()
// This is necessary so that IHostingEnvironment.ApplicationName has the right
// value and libraries depending on it (to load the dependency context, for example)
// work properly.
.UseSetting(WebHostDefaults.ApplicationKey, typeof(TStartup).Assembly.GetName().Name)
.UseContentRoot(ContentRoot)
.ConfigureServices(InitializeServices);
return new TestServer(builder);
}
protected virtual void InitializeServices(IServiceCollection services)
{
// Inject a custom application part manager. Overrides AddMvcCore() because that uses TryAdd().
var manager = new ApplicationPartManager();
foreach (var assembly in ApplicationAssemblies)
{
manager.ApplicationParts.Add(new AssemblyPart(assembly));
}
services.AddSingleton(manager);
services.AddSingleton(new TestServiceRegistrations
{
Before = ConfigureServicesBeforeStartup,
After = ConfigureServicesAfterStartup
});
}
}
}

View File

@ -0,0 +1,37 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
Work around https://github.com/NuGet/Home/issues/4412. MVC uses DependencyContext.Load() which looks next to a .dll
for a .deps.json. Information isn't available elsewhere. Need the .deps.json file for all web site applications.
-->
<PropertyGroup>
<!--
The functional tests act as the host application for all test websites. Since the CLI copies all reference
assembly dependencies in websites to their corresponding bin/{config}/refs folder we need to re-calculate
reference assemblies for this project so there's a corresponding refs folder in our output. Without it
our websites deps files will fail to find their assembly referenes.
-->
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' != 'netcoreapp2.0' ">
<!-- Work around https://github.com/dotnet/sdk/issues/926. Align with bitness of the web site projects. -->
<PlatformTarget>x86</PlatformTarget>
<!--
Work around https://github.com/Microsoft/vstest/issues/428 aka https://github.com/aspnet/Mvc/issues/5873.
Create the appropriate binding redirects.
-->
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
<ItemGroup>
<DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', '.deps.json'))" />
</ItemGroup>
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
</Target>
</Project>

View File

@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<Import Project="..\..\src\Microsoft.AspNetCore.Mvc.Testing\build\Microsoft.AspNetCore.Mvc.Testing.targets" />
<Import Project="..\..\src\Microsoft.AspNetCore.Mvc.Testing.Xunit\build\Microsoft.AspNetCore.Mvc.Testing.Xunit.targets" />
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
@ -11,39 +13,15 @@
<DefineConstants>$(DefineConstants);__RemoveThisBitTo__GENERATE_BASELINES</DefineConstants>
<DefineConstants>$(DefineConstants);FUNCTIONAL_TESTS</DefineConstants>
<!--
The functional tests act as the host application for all test websites. Since the CLI copies all reference
assembly dependencies in websites to their corresponding bin/{config}/refs folder we need to re-calculate
reference assemblies for this project so there's a corresponding refs folder in our output. Without it
our websites deps files will fail to find their assembly referenes.
-->
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' != 'netcoreapp2.0' ">
<!-- Work around https://github.com/dotnet/sdk/issues/926. Align with bitness of the web site projects. -->
<PlatformTarget>x86</PlatformTarget>
<!--
Work around https://github.com/Microsoft/vstest/issues/428 aka https://github.com/aspnet/Mvc/issues/5873.
Create the appropriate binding redirects.
-->
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test\XmlAssert.cs" />
<EmbeddedResource Include="compiler\resources\**\*" />
<!--
Work around https://github.com/Microsoft/vstest/issues/196. Execute tests with assemblies in the bin folder, not
a temporary location. Unable to find the web site project folders otherwise.
-->
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Mvc.Testing.Xunit\Microsoft.AspNetCore.Mvc.Testing.Xunit.csproj" />
<ProjectReference Include="..\WebSites\ApiExplorerWebSite\ApiExplorerWebSite.csproj" />
<ProjectReference Include="..\WebSites\ApplicationModelWebSite\ApplicationModelWebSite.csproj" />
<ProjectReference Include="..\WebSites\BasicWebSite\BasicWebSite.csproj" />
@ -78,15 +56,4 @@
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
</ItemGroup>
<!--
Work around https://github.com/NuGet/Home/issues/4412. MVC uses DependencyContext.Load() which looks next to a .dll
for a .deps.json. Information isn't available elsewhere. Need the .deps.json file for all web site applications.
-->
<Target Name="CopyDepsFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
<ItemGroup>
<DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', '.deps.json'))" />
</ItemGroup>
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
</Target>
</Project>

View File

@ -2,19 +2,24 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Encodings.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.WebEncoders.Testing;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class MvcEncodedTestFixture<TStartup> : MvcTestFixture<TStartup>
where TStartup : class
{
protected override void InitializeServices(IServiceCollection services)
protected override void ConfigureApplication(MvcWebApplicationBuilder<TStartup> builder)
{
base.InitializeServices(services);
services.AddTransient<HtmlEncoder, HtmlTestEncoder>();
services.AddTransient<JavaScriptEncoder, JavaScriptTestEncoder>();
services.AddTransient<UrlEncoder, UrlTestEncoder>();
base.ConfigureApplication(builder);
builder.ConfigureBeforeStartup(services =>
{
services.TryAddTransient<HtmlEncoder, HtmlTestEncoder>();
services.TryAddTransient<JavaScriptEncoder, JavaScriptTestEncoder>();
services.TryAddTransient<UrlEncoder, UrlTestEncoder>();
});
}
}
}

View File

@ -4,10 +4,8 @@
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class MvcSampleFixture<TStartup> : MvcTestFixture<TStartup>
where TStartup : class
{
public MvcSampleFixture()
: base("samples")
{
}
public MvcSampleFixture() : base("samples") { }
}
}

View File

@ -1,71 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class MvcTestFixture<TStartup> : IDisposable
public class MvcTestFixture<TStartup> : WebApplicationTestFixture<TStartup>
where TStartup : class
{
private readonly TestServer _server;
public MvcTestFixture()
: this(Path.Combine("test", "WebSites"))
: base(Path.Combine("test", "WebSites"))
{
}
protected MvcTestFixture(string solutionRelativePath)
: base(solutionRelativePath)
{
// RequestLocalizationOptions saves the current culture when constructed, potentially changing response
// localization i.e. RequestLocalizationMiddleware behavior. Ensure the saved culture
// (DefaultRequestCulture) is consistent regardless of system configuration or personal preferences.
using (new CultureReplacer())
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
var contentRoot = SolutionPathUtility.GetProjectPath(solutionRelativePath, startupAssembly);
var builder = new WebHostBuilder()
.UseContentRoot(contentRoot)
.ConfigureServices(InitializeServices)
.UseStartup(typeof(TStartup));
_server = new TestServer(builder);
}
Client = _server.CreateClient();
Client.BaseAddress = new Uri("http://localhost");
}
public HttpClient Client { get; }
public void Dispose()
protected override void ConfigureApplication(MvcWebApplicationBuilder<TStartup> builder)
{
Client.Dispose();
_server.Dispose();
}
protected virtual void InitializeServices(IServiceCollection services)
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
// Inject a custom application part manager. Overrides AddMvcCore() because that uses TryAdd().
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.FeatureProviders.Add(new ControllerFeatureProvider());
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
services.AddSingleton(manager);
base.ConfigureApplication(builder);
builder.ApplicationAssemblies.Clear();
builder.ApplicationAssemblies.Add(typeof(TStartup).GetTypeInfo().Assembly);
}
}
}