This content originally appeared on DEV Community and was authored by Pierre Bouillon
I’ve recently started writing about how I’m building PocketBase.Net in the Building PocketBase.Net Step by Step series.
However, I haven’t had the opportunity to go over what was already in place. In particular, having a robust testing suite with both unit and integration tests was a very important point for me, as the library could potentially be used by others to manage their data, which is often the most critical part of applications.
In this article, I will explain how I set up a working testing environment for my integration tests, using a dockerized PocketBase instance with TestContainers.
A short Introduction to Integration Testing
As software engineers, we are (I hope) used to testing our code. However, there are a lot of ways to test it, with the most common from my experience being:
- “Local” testing: you just developed a feature and you run it locally, in your environment, to ensure it is working
- Unit testing: you are asserting that your method, given a precise input and no (or simulated) dependencies, returns the expected result. It is usually written in three acts: Arrange (prepare the system), Act (perform the operation) and Assert (ensure that the result is the expected one).
In C#, a unit test can be as simple as:
public void Calculator_Multiply_ReturnsPositiveValueForNegativeNumbers()
{
// Arrange: the system is created in isolation
var calculator = new Calculator();
// Act: a single operation is performed
var result = calculator.Multiply(-4, -2);
// Assert: we test operation's result
result.ShouldBeGreaterThan(0);
}
However, unit tests can lack of context in more complex scenarios. For instance, you might be sure that a method is working in isolation, but when chained with many other method calls it might not. In the same way, your dependencies (email service, database, authentication system, etc.) might not be interacting with it correctly.
To ensure those cases are covered as well, you can write integration tests, which will give you a bit more confidence about how your code is behaving when considered in its globality, with its dependencies and external systems.
NOTE:
We often speak about test pyramid to represent the types of tests and their number.
If you were not already familiar with testing and want to dive deeper, I highly recommend you to read the resources on the Software Testing Guide by Martin Fowler.
Integration Testing in PocketBase.Net
In the case of PocketBase.Net, I wanted to be sure that the logic tested in my unit tests, was correctly integrating with PocketBase once plugged to it.
Creating a Testing PocketBase Instance
To ensure that the code works with PocketBase, I had to interact with a real instance. I considered two options:
- Running an instance somewhere, either locally or hosted.
- Creating a dockerized instance that would be destroyed once the tests are completed.
I picked the second option as I thought it was the best approach to still have an isolated and reproducible environment, even from the CI.
Introducing TestContainers
Fortunately, there is an awesome project for that named TestContainers.
TestContainers is a library that allows you to run a docker container programmatically in a few lines, using the builder pattern:
// Create a new instance of a container.
var container = new ContainerBuilder()
.WithImage("xxx/yyy:1.2.0")
.Build();
// Start the container.
await container.StartAsync();
All we need to get started with this library is to have Docker on our system, and an image to use.
Creating Our PocketBase Container
To get started with our dockerized version of PocketBase, we have to pick a Docker image from the many available on DockerHub:
In this article, as for PocketBase.Net, we will be using adrianmusante/pocketbase
.
NOTE:
Choose your image carefully as you might have some security requirements to consider.
Writing our builder only needs a few lines:
new ContainerBuilder()
.WithImage("adrianmusante/pocketbase:0.28.4")
.Build();
But we can actually go further with all the available options.
For instance, we can bind the ports, run migrations, define an administrators, and much more:
new ContainerBuilder()
.WithImage("adrianmusante/pocketbase:0.28.4")
// 👇 Bind port 8080
.WithPortBinding(PocketBasePort, true)
// 👇 Wait for the health check to respond
.WithWaitStrategy(
Wait.ForUnixContainer()
.UntilHttpRequestIsSucceeded(r => r
.ForPort(PocketBasePort)
.ForPath("/api/health")
.WithMethod(HttpMethod.Get)))
// 👇 Delete the container once shut down
.WithCleanUp(true)
// 👇 Setup an administrator
.WithEnvironment("POCKETBASE_ADMIN_EMAIL", AdminCredentials.Identity)
.WithEnvironment("POCKETBASE_ADMIN_PASSWORD", AdminCredentials.Password)
// 👇 Install and run migrations
.WithResourceMapping(
source: migrationsDirectory,
target: "/pocketbase/migrations")
.Build();
Now that our builder is defined, let’s see how to integrate it in our xUnit test suite!
Using the Container with xUnit
When sharing context between test classes, xUnit suggests using fixtures such as IClassFixture<>
.
This is especially useful since it supports having an asynchronous workflow during the setup of your test context, simply by implementing IAsyncLifetime
. In our case, running the container is an asynchronous operation since we have to wait for it to be up.
Let’s create our fixture for our container to be used in our tests:
public class PocketBaseFixture : IAsyncLifetime
{
...
public PocketBaseFixture()
{
// 1⃣ Create the container builder
_pocketBaseContainer = new ContainerBuilder()
...
.Build();
}
public async Task InitializeAsync()
{
// 2⃣ Start the container
await _pocketBaseContainer
.StartAsync()
.ConfigureAwait(false);
// 3⃣ Retrieve the container's details
var pocketBaseBaseUrl =
$"http://{_pocketBaseContainer.Hostname}:" +
$"{_pocketBaseContainer.GetMappedPublicPort(PocketBasePort)}";
// 4⃣ Interact with the container
ServiceProvider = new ServiceCollection()
.AddPocketBase(
serverUrl: new Uri(pocketBaseBaseUrl),
credentials: AdminCredentials)
.AddPocketBaseRepositories(scanningAssembly: typeof(TodoItemRecord).Assembly)
.BuildServiceProvider();
}
public async Task DisposeAsync()
{
// 5⃣ Stop everything properly
await _pocketBaseContainer
.StopAsync()
.ConfigureAwait(false);
}
}
With the fixture being defined, we now need to indicate xUnit to use it for our test class:
// 👇 Tells xUnit the fixture to use
public class AuthenticationTests(PocketBaseFixture fixture)
: IClassFixture<PocketBaseFixture>
{
[Trait("scenario", "happy path")]
[Fact(DisplayName = "Authentication should succeed with valid admin credentials")]
public async Task AuthenticateUsing_WithValidCredentials_ShouldReturnAuthenticatedUser()
{
// Arrange
var client = fixture.ServiceProvider.GetRequiredService<PocketBaseHttpClientWrapper>();
// Act
// 👇 Makes an HTTP call to our dockerized pocketbase instance
var user = await client.AuthenticateUsing(
fixture.AdminCredentials,
CancellationToken.None);
// Assert
user.ShouldNotBeNull();
user.Email.ShouldBe(fixture.AdminCredentials.Identity);
client.IsAuthenticated.ShouldBeTrue();
}
}
When running our test, we can now be sure that AuthenticateUsing
is working correctly with a real PocketBase instance:
Closing thoughts
In this article, we saw what integration tests are, how to take advantage of docker in C# with TestContainers and how to harness our container for our xUnit test suite.
I hope this article gave you some insights about how you could be using either PocketBase or TestContainers for your next projects!
If you would like to have a look at the PocketBase.Net integration tests to see the full code.
This content originally appeared on DEV Community and was authored by Pierre Bouillon