Walkthrough
This walkthrough will provide instructions to get from a generated ASP.Net Core application to adding your first MvcTesting test against the registration functionality.
You only need .Net Core and a text editor.
Create a directory for your new application in c:\temp\myapp
. This walkthrough uses
.Net SDK 2.1.2, which generates different code to the latest version. So start
by creating a c:\temp\myApp\global.json
file with the following contents:
{
"sdk": {
"version": "2.1.200"
}
}
Now execute the following from a command prompt:
cd c:\temp\myapp\
mkdir web
mkdir web.tests
cd web
dotnet new mvc --auth Individual
cd ..\web.tests
dotnet new xunit
dotnet add reference ..\web\web.csproj
dotnet add package MvcTesting
And if you want the solution too:
cd ..
dotnet new sln
dotnet sln add web\web.csproj
dotnet sln add web.tests\web.tests.csproj
MvcTesting is a thin wrapper around the Microsoft.AspNetCore.TestHost.TestServer
. In order to get this to work,
open the web.tests\web.tests.csproj
and add the following section at the end. A working example can be found
here: https://github.com/FlukeFan/MvcTesting/blob/master/example/web.tests/web.tests.csproj
<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>
Delete the file UnitTest1.cs
, and add a new file AccountRegistrationTests.cs
with the following:
using System.Threading.Tasks;
using MvcTesting.Html;
using web.Models.AccountViewModels;
using Xunit;
namespace web.tests
{
public class AccountRegistrationTests : IClassFixture<WebTest>
{
private WebTest _webTest;
public AccountRegistrationTests(WebTest webTest)
{
_webTest = webTest;
}
[Fact]
public async Task WhenRegistering_DisplaysForm()
{
var form = await _webTest.Client()
.GetAsync("/Account/Register")
.Form<RegisterViewModel>();
Assert.Equal("", form.GetText(f => f.Email));
}
}
}
This is what our first web test will look like. This relies on a couple of helper classes, so add
the following code to WebTest.cs
:
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using MvcTesting.AspNetCore;
namespace web.tests
{
public class WebTest : IDisposable
{
private TestServer _testServer;
public WebTest()
{
_testServer = new WebHostBuilder()
.UseEnvironment("Development")
.UseContentRoot(@"..\..\..\..\web")
.UseStartup<FakeStartup>()
.MvcTestingTestServer();
}
public SimulatedHttpClient Client()
{
return _testServer.MvcTestingClient();
}
public void Dispose()
{
using (_testServer) { }
}
}
}
… and add the following code to FakeStartup.cs
:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MvcTesting.AspNetCore;
using web.Models;
using web.Services;
namespace web.tests
{
public class FakeStartup : Startup
{
public FakeStartup(IConfiguration configuration) : base(configuration) { }
public override void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(typeof(UserManager<ApplicationUser>), UserManagerSpy.Instance);
services.AddSingleton(typeof(SignInManager<ApplicationUser>), SignInManagerSpy.Instance);
services.AddSingleton<IEmailSender>(EmailSenderSpy.Instance);
services.AddMvc(o => o.Filters.Add<CaptureResultFilter>());
}
}
}
Update the ConfigureServices in Startup.cs (in the web project) to make the ConfigureServices method virtual:
...
// This method gets called by the runtime. Use this method to add services to the container.
public virtual void ConfigureServices(IServiceCollection services)
{
...
In order to test some parts of the AccountController, we have to stub out some dependencies
during the tests. Add the test doubles for UserManagerSpy
, SignInManagerSpy
, and EmailSenderSpy
copied from the source code
here: https://github.com/FlukeFan/MvcTesting/blob/master/example/web.tests/Doubles.cs
You should now be able to run your first test:
cd web.tests
dotnet test
This test calls through the complete MVC stack to get the razor view, then scrapes the HTML result’s form elements into a strongly typed model. However, there is little or no logic in this controller action, so next we’ll add a more ambitious test. Add the following to AccountRegistrationTests.cs
:
using System.Net;
using Microsoft.AspNetCore.Mvc;
...
[Fact]
public async Task WhenDetailsEntered_DisplaysExternalForm()
{
var form = await _webTest.Client()
.GetAsync("/Account/Register")
.Form<RegisterViewModel>();
var response = await form
.SetText(m => m.Email, "unit.test@unit.test")
.SetText(m => m.Password, "Un!tTestPassw0rd")
.SetText(m => m.ConfirmPassword, "Un!tTestPassw0rd")
.Submit();
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var result = response.ActionResultOf<RedirectToActionResult>();
Assert.Equal("Home", result.ControllerName);
Assert.Equal("Index", result.ActionName);
var actualCreatedUser = UserManagerSpy.LastCreate.Item1;
var actualPassword = UserManagerSpy.LastCreate.Item2;
Assert.Equal("unit.test@unit.test", actualCreatedUser.UserName);
Assert.Equal("unit.test@unit.test", actualCreatedUser.Email);
Assert.Equal("Un!tTestPassw0rd", actualPassword);
var actualEmail = EmailSenderSpy.LastSendEmail.Item1;
var actualSubject = EmailSenderSpy.LastSendEmail.Item2;
var actualMessage = EmailSenderSpy.LastSendEmail.Item3;
Assert.Equal("unit.test@unit.test", actualEmail);
Assert.Equal("Confirm your email", actualSubject);
Assert.Contains(UserManagerSpy.EmailToken, actualMessage);
var actualSignInUser = SignInManagerSpy.LastSignIn.Item1;
var actualPersistent = SignInManagerSpy.LastSignIn.Item2;
Assert.Same(actualCreatedUser, actualSignInUser);
Assert.False(actualPersistent);
}
Run the tests again and you should now have two passing tests:
cd web.tests
dotnet test
To breakdown what’s actually happening in each part of the test:
var form = await _webTest.Client()
.GetAsync("/Account/Register")
.Form<RegisterViewModel>();
The above code ‘scrapes’ the HTML response into a strongly typed model. This model wraps a collection of key-value pairs that can be exmained to see their contents. In addition, these values can be used to form an HTTP POST equivalent to the POST request a real browser would send when the submit button is pressed.
var response = await form
.SetText(m => m.Email, "unit.test@unit.test")
.SetText(m => m.Password, "Un!tTestPassw0rd")
.SetText(m => m.ConfirmPassword, "Un!tTestPassw0rd")
.Submit();
The above code sets the form’s values and posts the form back to the web-server as a POST request.
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var result = response.ActionResultOf<RedirectToActionResult>();
Assert.Equal("Home", result.ControllerName);
Assert.Equal("Index", result.ActionName);
The above code checks the response is of the correct type. Since we added the
CaptureResultFilter
in the FakeStartup
, we can also examine the action result of
the request.
var actualCreatedUser = UserManagerSpy.LastCreate.Item1;
var actualPassword = UserManagerSpy.LastCreate.Item2;
Assert.Equal("unit.test@unit.test", actualCreatedUser.UserName);
Assert.Equal("unit.test@unit.test", actualCreatedUser.Email);
Assert.Equal("Un!tTestPassw0rd", actualPassword);
var actualEmail = EmailSenderSpy.LastSendEmail.Item1;
var actualSubject = EmailSenderSpy.LastSendEmail.Item2;
var actualMessage = EmailSenderSpy.LastSendEmail.Item3;
Assert.Equal("unit.test@unit.test", actualEmail);
Assert.Equal("Confirm your email", actualSubject);
Assert.Contains(UserManagerSpy.EmailToken, actualMessage);
var actualSignInUser = SignInManagerSpy.LastSignIn.Item1;
var actualPersistent = SignInManagerSpy.LastSignIn.Item2;
Assert.Same(actualCreatedUser, actualSignInUser);
Assert.False(actualPersistent);
Finally, because we setup UserManagerSpy
, SignInManagerSpy
, and EmailSenderSpy
in FakeStartup
, we can also verify the injected dependencies were used correctly.