Skip to main content

Our first UI integration test

Now that we have all the tools we need, let's start writing integration tests

Folder and file structure

First let's create a folder called Pages and a folder Customer inside that folder. This is done to mirror the folder structure of the Customers.WebApp project and help us see at a glance what's being tested.

Our tests will be separated in classes just like our pages, but due to the collection definition in xUnit, they will all share their context.

First test: Creating a customer

Our first test will be Create_ShouldCreateCustomer_WhenDataIsValid and it will follow the same method structure as all of our other tests.

We first need to create the AddCustomerTests.cs class in which the tests will live.

AddCustomerTests.cs
[Collection("Test collection")]
public class AddCustomerTests
{
private readonly TestingContext _testingContext;

public AddCustomerTests(TestingContext testingContext)
{
_testingContext = testingContext;
}
}

And then create the main structure for the test

AddCustomerTests.cs
[Fact]
public async Task Create_ShouldCreateCustomer_WhenDataIsValid()
{
// Arrange

// Act

// Assert

}

Before we start implementing the main sections we need to implement what lives outside of the test.

InitializeAsync

Before a test runs we need to create a browser page. We need to do this outside of the main test code because we will be closing the page after the test and we don't want it to stay open if a test fails.

First let's implement the IAsyncLifetime interface and add the page creation code in InitializeAsync.

private IPage _page = default!;

public async Task InitializeAsync()
{
_page = await _testingContext.Browser.NewPageAsync();
}

Arrange

In the Arrange section we need to:

  • Create a new Playwright browser page and navigate to the right page
  • Create any data we need by calling the database directly or using the interface
  • Setup any request or expected data objects
  • Setup the fake GitHub API user

First let's create the new page and navigate to /add-customer.

var page = await _testingContext.Browser.NewPageAsync();
await page.GotoAsync($"{TestingContext.AppUrl}/add-customer");

_testingContext.GitHubApiServer.SetupUser("nickchapsas");

Act

In the Act section we will make all the actions needed in the page to make the thing that the test is testing for, happen. In this specific case we will fill in all the data and click the submit button.

We will use the exact same code we used in the previous section

AddCustomerTests.cs
await page.Locator("id=fullname").FillAsync(customer.FullName);
await page.Locator("id=email").FillAsync(customer.Email);
await page.Locator("id=github-username").FillAsync(customer.GitHubUsername);
await page.Locator("id=dob").FillAsync(customer.DateOfBirth.ToString("yyyy-MM-dd"));
await page.Locator("text=Submit").ClickAsync();

Assert

There are a couple of ways to assert that the action happened successfully. We can either navigate the listing page or the "get by id" page and validate the data there. However this might not always be possible, so creating a database connection, querying for the data and asserting the results is also a viable alternative.

Let's go back to the TestingContext and add setup the IDbConnectionFactory needed to create the database connection.

First let's add the IDbConnectionFactory as a property:

public IDbConnectionFactory Database { get; private set; }

And then let's initialize a connection in the InitializeAsync method:

public async Task InitializeAsync()
{
Database = new NpgsqlConnectionFactory(
"Server=localhost;Port=5435;Database=mydb;User ID=workshop;Password=changeme;");

GitHubApiServer.Start(9850);
_dockerService.Start();

_playwright = await Playwright.CreateAsync();
var browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
SlowMo = 1000,
Headless = false
});

Browser = await browser.NewContextAsync(new BrowserNewContextOptions
{
IgnoreHTTPSErrors = true
});
}

Now we have two options. We either write the raw queries using Dapper and map the results to the CustomerDto and validate against that or we use the CustomerRepository that already exists in the Web App project and use that to make all our database actions.

The pragmatic approach here is to use the CustomerRepository but you can follow whichever approach you want in this case.

Let's go ahead and add a field of type ICustomerRepository in the AddCustomerTests class and initialize it using the IDbConnectionFactory.

private readonly TestingContext _testingContext;
private readonly ICustomerRepository _customerRepository;

public AddCustomerTests(TestingContext testingContext)
{
_testingContext = testingContext;
_customerRepository = new CustomerRepository(_testingContext.Database);
}

We can now use the GetAsync method to get the user that we just created by id or the GetAllAsync method and isolate it assuming it is the only one.

In order to get the ID of the newly created customer we need to read the href attribute value of the here text on the successful creation page:

Since this is a "safer" approach, because it doesn't assume that this is the only element in the database, we will go ahead with that.

var href = await page.Locator("text='here'").GetAttributeAsync("href");
var href = await customerLink!.GetAttributeAsync("href");
var customerIdText = href!.Replace("/customer/", string.Empty);
var customerId = Guid.Parse(customerIdText);

The code above will give up the id of the customer we just created and we can now use that to get the user for the database and asset their properties.

var createdCustomer = await _customerRepository.GetAsync(customerId);

createdCustomer.Should().NotBeNull();
createdCustomer!.FullName.Should().Be("Nick Chapsas");
createdCustomer.Email.Should().Be("[email protected]");
createdCustomer.GitHubUsername.Should().Be("nickchapsas");
createdCustomer.DateOfBirth.Should().Be(new DateTime(1993, 9, 22));

DisposeAsync

Here all we need to do is close the browser page and reset the database.

Now it's time to add Respawn into the project to manage the database reset after each test. The approach is exactly the same as in the API integration tests.

TestingContext.cs
private readonly DbConnection _respawnDbConnection = default!;
private Respawner _respawner = default!;

private async Task InitializeRespawner()
{
_respawnDbConnection = await Database.CreateConnectionAsync();
_respawner = await Respawner.CreateAsync(_respawnDbConnection, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
SchemasToInclude = new[] { "public" }
});
}

public async Task ResetDatabaseAsync()
{
await _respawner.ResetAsync(_respawnDbConnection);
}
private readonly TestingContext _testingContext;
private readonly Func<Task> _databaseReset;

public AddCustomerTests(TestingContext testingContext)
{
_testingContext = testingContext;
_databaseReset = testingContext.ResetDatabaseAsync;
}

public Task DisposeAsync() => _resetDatabase();
public async Task DisposeAsync()
{
await page.CloseAsync();
_resetDatabase();
}

And that's it! We can now run the test and watch it pass!

Using Bogus

The last thing left to do is to use Bogus to replace the manual creation of data. The approach is exactly the same as before.

First we need to create the generator:

private readonly Faker<CustomerDto> _customerGenerator = new Faker<CustomerDto>()
.RuleFor(x => x.Id, Guid.NewGuid)
.RuleFor(x => x.Email, f => f.Person.Email)
.RuleFor(x => x.FullName, f => f.Person.FullName)
.RuleFor(x => x.DateOfBirth, f => f.Person.DateOfBirth.Date)
.RuleFor(x => x.GitHubUsername, f => f.Person.UserName.Replace(".", "").Replace("-", "").Replace("_", ""));

You can also use the Customer type found under Models but you need to handle the DateOnle->DateTime type match with a special condition.

And then we need to update our test to create a customer and use it:

private IPage _page = default!;

public async Task InitializeAsync()
{
_page = await _testingContext.Browser.NewPageAsync();
}

[Fact]
public async Task Create_ShouldCreateCustomer_WhenDataIsValid()
{
// Arrange
await page.GotoAsync($"{TestingContext.AppUrl}/add-customer");

var customer = _customerGenerator.Generate();
_testingContext.GitHubApiServer.SetupUser(customer.GitHubUsername);

// Act
await page.Locator("id=fullname").FillAsync(customer.FullName);
await page.Locator("id=email").FillAsync(customer.Email);
await page.Locator("id=github-username").FillAsync(customer.GitHubUsername);
await page.Locator("id=dob").FillAsync(customer.DateOfBirth.ToString("yyyy-MM-dd"));
await page.Locator("text=Submit").ClickAsync();

// Assert
var href = await page.Locator("text='here'").GetAttributeAsync("href");
var customerIdText = href!.Replace("/customer/", string.Empty);
var customerId = Guid.Parse(customerIdText);

var createdCustomer = await _customerRepository.GetAsync(customerId);
createdCustomer.Should().BeEquivalentTo(customer, x => x.Excluding(p => p.Id));
}

public async Task DisposeAsync()
{
await page.CloseAsync();
_resetDatabase();
}

And that's it! We now have a full automated UI-based integration test in place with its own docker-specific database, fake GitHub API and service with database resets after every test!

Let's go ahead and write more tests!