Using WireMock.net for Integration Testing

By Alex Hyett on in Software Development

Last week I showed you how you can use Wiremock in a docker container to mock API calls that your application uses. This week is on a similar theme, but this time using WireMock.net to help with your integration tests.

Unlike WireMock which is Java-based, as you can guess from the name WireMock.Net is written in .NET and based on mock4net. Everything you can do in Wiremock you can also do in WireMock.net. This also includes hosting it in a docker container like we did in the last post (the ASCII art is missing though).

In this tutorial, we are going to be looking at using WireMock.net to mock API calls in our integration tests.

If you already have a project set up you might want to skip to “Set up the WebApplicationFactory”.

Getting started

First of all, we need a .Net project to test. For this project I am just going to create a new .Net Core Web API from the templates:

dotnet new webapi

This will generate a simple API that gives us a random weather forecast.

Running dotnet run and then calling the GET endpoint https://localhost:5001/WeatherForecast returns the following:

[
  {
    "date": "2021-05-22T10:22:17.824198+01:00",
    "temperatureC": 36,
    "temperatureF": 96,
    "summary": "Chilly"
  },
  {
    "date": "2021-05-23T10:22:17.824551+01:00",
    "temperatureC": 25,
    "temperatureF": 76,
    "summary": "Warm"
  },
  {
    "date": "2021-05-24T10:22:17.824557+01:00",
    "temperatureC": 20,
    "temperatureF": 67,
    "summary": "Hot"
  },
  {
    "date": "2021-05-25T10:22:17.824557+01:00",
    "temperatureC": 14,
    "temperatureF": 57,
    "summary": "Bracing"
  },
  {
    "date": "2021-05-26T10:22:17.824558+01:00",
    "temperatureC": -15,
    "temperatureF": 6,
    "summary": "Sweltering"
  }
]

You can tell it is random, as I wouldn’t consider -15°C, “Sweltering”.

The actual code that generates this looks like this:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering",
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

There is one subtle change that has to be made to be able to run integration tests against this. That is to replace the IHostBuilder in Program.cs with an IWebHostBuilder.

Your Program.cs should look like this:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace WireMock.Net.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();
    }
}

Adding an API to Test

The whole point of an integration test is to test the integration between components. So we need to add an API to our project so that we can mock it using wiremock.net.

As this API is already returning random weather, we are going to add in real weather API to get back realistic data. For this, we are going to use 7timer’s weather API. This API is free and doesn’t require any authentication.

The API we are going to call is this one:

https://www.7timer.info/bin/civillight.php?lat=51.5074&lon=0.1278&ac=0&unit=metric&output=json&tzshift=0

This returns back weather for the next 7 days for London (51.5074° N, 0.1278° W). The format of the data we get back is slightly different so we will need to format it before returning the data. This is what the first few entries in the array look like:

{
  "product": "civillight",
  "init": "2021052100",
  "dataseries": [
    {
      "date": 20210521,
      "weather": "lightrain",
      "temp2m": { "max": 11, "min": 9 },
      "wind10m_max": 4
    },
    {
      "date": 20210522,
      "weather": "lightrain",
      "temp2m": { "max": 10, "min": 7 },
      "wind10m_max": 3
    },
    {
      "date": 20210523,
      "weather": "lightrain",
      "temp2m": { "max": 12, "min": 4 },
      "wind10m_max": 4
    }
  ]
}

For our API we need the following information:

  • dataseries[].date - The date which we can just convert to a DateTime.
  • dataseries[].temp2m - This is the min max temperature in Celsius so we can just use this as is. We can update our API to return both.
  • dataseries[].weather - The weather summary, again we can use as is.

I am going to use Refit for calling the API. If you haven’t used Refit it is a great tool for standardising your API calls and saves on a lot of boilerplate code for calling your API. So add the following packages to your API project:

<PackageReference Include="Refit" Version="6.0.38"/>
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.38"/>

Next we need to create an interface for our API:

using System.Threading;
using System.Threading.Tasks;

using Refit;

using WireMock.Net.Api.Client.Model;

namespace WireMock.Net.Api.Client
{
    public interface IWeatherClient
    {
        [Get("/bin/civillight.php?lat={latitude}&lon={longitude}&ac=0&unit=metric&output=json&tzshift=0")]
        Task<ApiResponse<WeatherResponse>> GetWeatherAsync(decimal latitude, decimal longitude, CancellationToken ct);
    }
}

My WeatherResponse class looks like this:

using System.Collections.Generic;

using Newtonsoft.Json;

namespace WireMock.Net.Api.Client.Model
{
    public class WeatherResponse
    {
        [JsonProperty("product")]
        public string Product { get; set; }

        [JsonProperty("init")]
        public string Init { get; set; }

        [JsonProperty("dataseries")]
        public IEnumerable<WeatherData> Dataseries { get; set; }
    }

    public class WeatherData
    {
        [JsonProperty("date")]
        public int Date { get; set; }

        [JsonProperty("weather")]
        public string Weather { get; set; }

        [JsonProperty("temp2m")]
        public Temperature Temp2m { get; set; }
    }

    public class Temperature
    {
        [JsonProperty("max")]
        public int Max { get; set; }

        [JsonProperty("min")]
        public int Min { get; set; }
    }
}

We are also going to set up a configuration section in our appsettings.json so that the base address can be mocked later.

{
  "WeatherClient": {
    "BaseAddress": "https://www.7timer.info",
    "TimeoutSeconds": 30
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

So now we need to set up the refit HttpClient. We can do this inside ConfigureServices. To make things nicer I often put the code inside an extension method:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddWeatherClient(this IServiceCollection services, IConfiguration configuration)
    {
        var settings = configuration.GetSection("WeatherClient").Get<WeatherSettings>();
        services.AddRefitClient<IWeatherClient>().ConfigureHttpClient((sp, client) =>
        {
            client.BaseAddress = new Uri(settings.BaseAddress);
            client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        });
        return services;
    }

Then you just need to add AddWeatherClient(Configuration).

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddWeatherClient(Configuration);
}

Lastly, we are going to call our API from our controller.

Normally I would create a service for this but keeping it simple for brevity. Our controller now looks like this:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IWeatherClient _weatherClient;
    public WeatherForecastController(ILogger<WeatherForecastController> logger, IWeatherClient weatherClient)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _weatherClient = weatherClient ?? throw new ArgumentNullException(nameof(weatherClient));
    }
    [HttpGet]
    public async Task<IActionResult> GetAsync(CancellationToken ct)
    {
        var weatherResponse = await _weatherClient.GetWeatherAsync(51.5074m, 0.1278m, ct);
        if (!weatherResponse.IsSuccessStatusCode || weatherResponse?.Content?.Dataseries == null)
        {
            _logger.LogError("Unexpected status code from Weather API {StatusCode}", weatherResponse.StatusCode);
            return new StatusCodeResult(StatusCodes.Status500InternalServerError);
        }
        return Ok(weatherResponse.Content.Dataseries.Select(weather => new WeatherForecast
        {
             Date = DateTime.ParseExact(weather.Date.ToString(), "yyyyMMdd", CultureInfo.InvariantCulture),
             TemperatureC = new Temperature { Min = weather.Temp2m.Min, Max = weather.Temp2m.Max },
             Summary = weather.Weather
        }));
    }
}

Now when we run our API and call the get endpoint we will get back the results from the actual API.

[
  {
    "date": "2021-05-21T00:00:00",
    "temperatureC": { "min": 9, "max": 11 },
    "temperatureF": { "min": 48, "max": 51 },
    "summary": "lightrain"
  },
  {
    "date": "2021-05-22T00:00:00",
    "temperatureC": { "min": 7, "max": 10 },
    "temperatureF": { "min": 44, "max": 49 },
    "summary": "lightrain"
  },
  {
    "date": "2021-05-23T00:00:00",
    "temperatureC": { "min": 4, "max": 12 },
    "temperatureF": { "min": 39, "max": 53 },
    "summary": "lightrain"
  },
  {
    "date": "2021-05-24T00:00:00",
    "temperatureC": { "min": 7, "max": 12 },
    "temperatureF": { "min": 44, "max": 53 },
    "summary": "rain"
  },
  {
    "date": "2021-05-25T00:00:00",
    "temperatureC": { "min": 7, "max": 12 },
    "temperatureF": { "min": 44, "max": 53 },
    "summary": "rain"
  },
  {
    "date": "2021-05-26T00:00:00",
    "temperatureC": { "min": 6, "max": 15 },
    "temperatureF": { "min": 42, "max": 58 },
    "summary": "rain"
  },
  {
    "date": "2021-05-27T00:00:00",
    "temperatureC": { "min": 6, "max": 18 },
    "temperatureF": { "min": 42, "max": 64 },
    "summary": "clear"
  }
]

Because we are calling the actual API we can’t test what happens with a different response from the API. This is why we need to mock out this external dependency in our integration tests.

Creating our Integration Test Project

I prefer to have my projects laid out in the following format:

src/
├── Project/
│   ├── Project.csproj
│
test/
├── Project.Tests/
│   ├── Project.Tests.csproj
Project.sln

After moving the project into a separate src folder we can then use the dotnet templates to create our xunit test project inside our test project folder.

dotnet new xunit

Lastly back in the route folder we are going to create our solution file and add our projects to it.

dotnet new sln
dotnet sln add src/*
dotnet sln add test/*

Next we need to add a few packages to our test project:

<PackageReference Include="WireMock.Net" Version="1.4.15"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6"/>
<PackageReference Include="FluentAssertions" Version="5.10.3"/>

We also need to add a reference to our main project as well as an appsettings.test.json file and a Resources folder that we will add in a moment. Your test project should look something like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp5.0</TargetFramework>
    <RootNamespace>WireMock.Net.Test</RootNamespace>
    <IsPackable>false</IsPackable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0"/>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6"/>
    <PackageReference Include="xunit" Version="2.4.0"/>
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0"/>
    <PackageReference Include="coverlet.collector" Version="1.2.0"/>
    <PackageReference Include="WireMock.Net" Version="1.4.15"/>
    <PackageReference Include="FluentAssertions" Version="5.10.3"/>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\WireMock.Net.Api\WireMock.Net.Api.csproj" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.test.json">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
    <Content Include="Resources\*.*">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
</Project>

Now go and create an appsettings.test.json file and a Resources folder. The Resources folder is where we are going to put the mocked responses for our API.

In your appsettings.test.json file put the following:

{
  "WeatherClient": {
    "BaseAddress": "http://localhost:50000"
  }
}

We will set up our wiremock on this port in a bit.

Set up the WebApplicationFactory

To be able to replace the call to our actual API with our WireMock version we need our API to pick up our appsettings.test.json file so that we can replace the BaseAddress. To do this we are going to create a WebApplicationFactory and use it as a Class fixture in our tests.

using System.IO;

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;

using WireMock.Net.Api;

namespace WireMock.Net.Test.Infrastructure
{
    public class ApiWebFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
    {
        protected override IWebHostBuilder CreateWebHostBuilder() =>
            WebHost.CreateDefaultBuilder()
            .ConfigureAppConfiguration((context, config) =>
            {
                config
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json", true, true)
                    .AddJsonFile("appsettings.test.json", false, false);
            })
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseKestrel()
            .UseStartup<Startup>();
    }
}

Next, we are going to create a base class for our integration tests to simplify making calls to our API and reading the responses.

Note: we need to add an XUnit Collection so that the tests are run sequentially, otherwise we are going to have issues with conflicting port numbers when running our test.

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Mvc.Testing;

using Newtonsoft.Json;

using WireMock.Net.Api;

using Xunit;

namespace WireMock.Net.Test.Infrastructure
{
    [Collection("sequential")]
    public abstract class IntegrationBase : IClassFixture<ApiWebFactory<Startup>>
    {
        protected HttpClient HttpClient { get; }
        protected IntegrationBase(WebApplicationFactory<Startup> factory)
        {
            HttpClient = factory.CreateClient();
            factory.Server.AllowSynchronousIO = true;
        }

        protected HttpRequestMessage CreateGetRequest(string url)
        {
            return new HttpRequestMessage(HttpMethod.Get, url);
        }

        protected static async Task<T> ReadResponseAsync<T>(HttpResponseMessage response)
        {
            var result = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<T>(result);
        }
    }
}

Then we need to create our mock API for the weather endpoint.

using System;
using System.IO;

using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;

namespace WireMock.Net.Test.Infrastructure
{
    public class WeatherFixture : IDisposable
    {
        protected readonly WireMockServer _mockApi;

        public WeatherFixture()
        {
            _mockApi = WireMockServer.Start(50000);
        }

        public void Dispose()
        {
            _mockApi.Stop();
        }

        public void Reset()
        {
            _mockApi.Reset();
        }

        public IRequestBuilder SetupGetWeather(string responseBodyResource, int statusCode = 200)
        {
            var request = Request.Create()
                .UsingGet()
                .WithPath("/bin/civillight.php*");

            var responseBody = string.IsNullOrWhiteSpace(responseBodyResource) ? new byte[0] : File.ReadAllBytes(responseBodyResource);

            _mockApi.Given(request)
                .RespondWith(
                    Response.Create()
                    .WithStatusCode(statusCode)
                    .WithHeader("content-type", "application/json")
                    .WithBody(responseBody)
                );

            return request;
        }
    }
}

In this class, we are starting the WireMockServer on port 50000. This is so that we know that our mock API is going to be running on localhost:50000 when we run our tests.

The SetupGetWeather method is used to set up the expected request and response. In our case, anything that matches /bin/civillight.php at the start is going to be mocked.

Then have the response, which is retrieved from a file and returned in the body. Remember that Resources folder we created earlier? That is where we are going to put our JSON responses.

Create a file called success.json in the Resources folder and put the successful response from the weather API in there.

{
  "product": "civillight",
  "init": "2021052100",
  "dataseries": [
    {
      "date": 20210521,
      "weather": "lightrain",
      "temp2m": { "max": 11, "min": 9 },
      "wind10m_max": 4
    },
    {
      "date": 20210522,
      "weather": "lightrain",
      "temp2m": { "max": 10, "min": 7 },
      "wind10m_max": 3
    },
    {
      "date": 20210523,
      "weather": "lightrain",
      "temp2m": { "max": 12, "min": 4 },
      "wind10m_max": 4
    },
    {
      "date": 20210524,
      "weather": "rain",
      "temp2m": { "max": 12, "min": 7 },
      "wind10m_max": 3
    },
    {
      "date": 20210525,
      "weather": "rain",
      "temp2m": { "max": 12, "min": 7 },
      "wind10m_max": 3
    },
    {
      "date": 20210526,
      "weather": "rain",
      "temp2m": { "max": 15, "min": 6 },
      "wind10m_max": 3
    },
    {
      "date": 20210527,
      "weather": "clear",
      "temp2m": { "max": 18, "min": 6 },
      "wind10m_max": 3
    }
  ]
}

Lastly we create our Integration test and make sure we get back the expected response:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

using FluentAssertions;

using WireMock.Net.Api;
using WireMock.Net.Test.Infrastructure;

using Xunit;

namespace WireMock.Net.Test
{
    public class IntegrationTest : IntegrationBase, IDisposable
    {
        private readonly WeatherFixture _weatherFixture;
        public IntegrationTest(ApiWebFactory<Startup> factory) : base(factory)
        {
            _weatherFixture = new WeatherFixture();
        }

        public void Dispose()
        {
            _weatherFixture.Reset();
            _weatherFixture.Dispose();
        }

        [Fact]
        public async Task Given_weather_api_successful_returns_weather()
        {
            // Arrange
            _weatherFixture.SetupGetWeather("Resources/success.json");

            // Act
            var request = CreateGetRequest("/weatherforecast");
            var result = await HttpClient.SendAsync(request);

            // Assert
            result.StatusCode.Should().Be(HttpStatusCode.OK);
            var response = await ReadResponseAsync<IEnumerable<WeatherForecast>>(result);
            response.Should().HaveCount(7);

            response.Should().Contain(x =>
                x.Date == DateTime.Parse("2021-05-25") &&
                x.Summary == "rain" &&
                x.TemperatureC.Max == 12 &&
                x.TemperatureC.Min == 7);
        }

        [Fact]
        public async Task Given_weather_api_unsuccessful_returns_500()
        {
            // Arrange
            _weatherFixture.SetupGetWeather(null, 503);

            // Act
            var request = CreateGetRequest("/weatherforecast");
            var result = await HttpClient.SendAsync(request);

            // Assert
            result.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
        }
    }
}

Now run the tests using dotnet test and you should see that your API is calling the mocked endpoint.

info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
      Start processing HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
      Sending HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101]
      Received HTTP response headers after 107.6376ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
      End processing HTTP request after 117.0377ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
      Start processing HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
      Sending HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101]
      Received HTTP response headers after 13.8969ms - 503
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
      End processing HTTP request after 14.0039ms - 503
fail: WireMock.Net.Api.Controllers.WeatherForecastController[0]
      Unexpected status code from Weather API ServiceUnavailable

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 832 ms

Final Comments

As I showed in my last post you can easily mock an API using WireMock in a docker container. However, if you want to be able to test different responses using the same endpoint then, you need to set up Wiremock.net directly in your integration tests.

You can find the complete code for this tutorial on my GitHub page.

If you have any questions let me know in the comments below.


Was this post useful?
If you found this post useful and would like to support me, you can do so by buying me a coffee. Donations help keep this blog ad-free.

ALSO ON ALEXHYETT.COM

Dealing with Imposter Syndrome as a Software Developer

Dealing with Imposter Syndrome as a Software Developer

  • 28 May 2021
I have been a professional software developer for over a decade and I have been writing code for over 25 years. However, sometimes I still…
Mocking API calls using WireMock

Mocking API calls using WireMock

  • 14 May 2021
It is rare in software development that you are building something in complete isolation from everything else. Generally, you are going to…
Using ngrok to test local websites and APIs

Using ngrok to test local websites and APIs

  • 07 May 2021
Often when I am creating a new website, I want to see how it is going to look on an actual device like my phone or tablet. You can use…
Using GitHub Actions to Deploy to S3

Using GitHub Actions to Deploy to S3

  • 26 March 2021
Recently I went through the process of setting up Drone CI on my Raspberry Pi. The plan was to use my Raspberry Pi as a build server for…
Getting Started with AWS Step Functions

Getting Started with AWS Step Functions

  • 12 March 2021
I have recently been looking into AWS Step Functions. For those not familiar with them, Step Functions are Amazon’s way of providing a state…
Useful Docker Commands Worth Saving

Useful Docker Commands Worth Saving

  • 12 February 2021
I use docker every day. All the applications I write at work or at home end up in docker containers. Most of the time though, I am only…
Grafana Monitoring on a Raspberry Pi

Grafana Monitoring on a Raspberry Pi

  • 28 January 2021
As you might have seen from my last few posts I have quite a lot running on my Raspberry Pi. I am currently using a Raspberry Pi 2 B which…

Alex Hyett
WRITTEN BY

Alex Hyett

Software Developer, Entrepreneur, Father, and Husband. Engineering Lead at Checkout.com.

Want to get in touch? You can find me here:


Join the Newsletter

Subscribe to get my latest content by email.

    I won't send you spam. Unsubscribe at any time.