Creating the walking skeleton - Part 2
In the previous article, we started the process of creating the “walking skeleton” - our first end-to-end test that exercises the entire system. In this post, we continue where we left off and complete our first test before we get started with the work of making the test pass.
Preparing our test server
To be able to run our end-to-end tests in memory, we need a test server that can be created and disposed of cheaply. The WebApplicationFactory<TEntryPoint>
provides a means for us to bootstrap a test server, with TEntryPoint
being the entry point class for the system under test.
Playwright does not work with Blazor out of the box as yet, therefore we shall need to create a custom implementation of the web application factory to bootstrap our tests. Thankfully for us, Daniel Donbavand already wrote a blog entry on this and we shall use this as our guide for our implementation. We shall call our factory class the BlazorApplicationFactory
:
public class BlazorApplicationFactory: WebApplicationFactory<Program>
A key method for us to override is the CreateHost
method, lifted from Daniel Donbavand’s article previously mentioned. According to the documentation, this “creates the IHost
with the bootstrapped application” in the provided IHostBuilder
instance.
protected override IHost CreateHost(IHostBuilder builder)
{
// Create the host for TestServer now before we
// modify the builder to use Kestrel instead.
var testHost = builder.Build();
// Modify the host builder to use Kestrel instead
// of TestServer so we can listen on a real address.
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());
// Create and start the Kestrel server before the test server,
// otherwise due to the way the deferred host builder works
// for minimal hosting, the server will not get "initialized
// enough" for the address it is listening on to be available.
// See https://github.com/dotnet/aspnetcore/issues/33846.
_host = builder.Build();
_host.Start();
// Extract the selected dynamic port out of the Kestrel server
// and assign it onto the client options for convenience so it
// "just works" as otherwise it'll be the default http://localhost
// URL, which won't route to the Kestrel-hosted HTTP server.
var server = _host.Services.GetRequiredService<IServer>();
var addresses = server.Features.Get<IServerAddressesFeature>();
ClientOptions.BaseAddress = addresses!.Addresses
.Select(x => new Uri(x))
.Last();
// Return the host that uses TestServer, rather than the real one.
// Otherwise the internals will complain about the host's server
// not being an instance of the concrete type TestServer.
// See https://github.com/dotnet/aspnetcore/pull/34702.
testHost.Start();
return testHost;
}
Writing our first end-to-end test
As previously discussed, our first end-to-end test shall test the scenario where the bidder joins the auction but does not submit a bid until the auction ends. For our tests, the following Nuget packages shall be necessary:
The Bogus library enables fake data generation, and FluentAssertions does as the name suggests - it provides us with a way to more elegantly write assertions in tests. The Playwright and xUnit libraries provide the core abstractions required to hold our tests together.
The first thing to note is that our test class needs to inherit from the PageTest
class in the Playwright library to enable page assertions. The class must also be decorated with IClassFixture<BlazorApplicationFactory>
so that it has access to the bootstraper.
When we decorate the test class with the IClassFixture<T>
interface, we tell the xUnit runtime that we would like to use a shared instance of type T
for all tests in the class. In this case, we’re saying that we want a single instance of the BlazorApplicationFactory
, shared across all runs of the different tests that may be found on the test class. In xUnit, a new instance of the test class is created before each test is run. Adding the IClassFixture<BlazorApplicationFactory>
decoration ensures that the same instance of the BlazorApplicationFactory
is shared across all test runs.
global using Microsoft.Playwright;
using AuctionSniper.IntegrationTests.Helpers;
using Microsoft.Playwright.NUnit;
using Bogus;
using FluentAssertions;
namespace AuctionSniper.IntegrationTests;
public class SniperEndToEndTests: PageTest,
IClassFixture<BlazorApplicationFactory>
{
private readonly string _serverAddress;
private readonly Faker _faker;
public SniperEndToEndTests(BlazorApplicationFactory applicationFactory)
{
_serverAddress = applicationFactory.ServerAddress;
_faker = new Faker();
}
[Fact]
public async Task SniperCanJoinAuctionUntilAuctionCloses()
{
//Arrange
using var playwright = await Microsoft.Playwright
.Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
var itemCode = _faker.Commerce.ProductAdjective();
var maxPrice = _faker.Commerce.Price();
var expectedStatus = "Bidding";
//Act
await page.GotoAsync(_serverAddress);
await page.GetByRole(AriaRole.Button,
new() { Name = "New Sniper" }).ClickAsync();
var dialog = page.Locator("[role='new-sniper-dialog']");
await Expect(dialog).ToBeVisibleAsync();
await dialog.GetByLabel("Item Code").FillAsync(itemCode);
await dialog.GetByRole(AriaRole.Button,
new() { Name = "Snipe"}).ClickAsync();
//Assert
var lastRow = page.Locator(".auction-row").Last;
lastRow.Should().NotBeNull();
await Expect(lastRow).ToHaveTextAsync(itemCode);
await Expect(lastRow).ToHaveTextAsync(maxPrice.ToString());
await Expect(lastRow).ToHaveTextAsync(expectedStatus);
}
}
In our test above, we use the Bogus library to create a dummy auction item. Using the Playwright library, we access the web page on which auctions can be joined. To join an auction, we click on the “New Sniper“ button and bring up the dialog box on which the item code and maximum bid price are entered. Once these are entered, the “Snipe” button is clicked, at which point the expectation is that a message is sent to the auction server to register the new participant. When our attempt to join the auction has been successful, we expect that a new row with the latest item details shall be visible on the table on the main screen.
Summary
In this article we setup our custom implementation of the WebApplicationFactory<TEntryPoint>
so that it can be used with Playwright.Net to test a Blazor application. We introduced the concept of IClassFixture
in xUnit, used to share context across tests in the same test class. Most importantly, we wrote our first test - exercised from the frontend but expected to run all the way to the auction server.
In the next installment, we shall build on the work done in this article, putting the necessary components together to make our first end-to-end test pass.