In the first part about Software Testing in a .NET Core 3.1 web application we explained unit, integration and end-to-end tests. To finish this series, we will discuss about health checks and testing in a continuous delivery pipeline.
These tests, as the name suggests, has the objective of validating if our project is alright. In our case, our API consumes and writes in a database, so in order for our API works healthy it must be connected to our database.
We can perform a simple check by just trying the command SELECT 1 on our database. If it succeeds it means we are connected, so we return healthy as a response, otherwise unhealthy. Startup.cs
public class Startup { public void ConfigureServices(IServiceCollection services) { /* * Your code.... */ services.AddHealthChecks().AddCheck("my-database-name", new DatabaseHealthCheck("my-database-connection-string")); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { /* * Your code.... */ app.UseHealthChecks("/health", new HealthCheckOptions { AllowCachingResponses = false, ResponseWriter = async (c, r) => { c.Response.ContentType = "application/json"; var results = r.Entries.Select(pair => { return KeyValuePair.Create(pair.Key, new ResponseResults { Status = pair.Value.Status.ToString(), Description = pair.Value.Description, Duration = pair.Value.Duration.TotalSeconds.ToString() + "s", ExceptionMessage = pair.Value.Exception != null ? pair.Value.Exception.Message : "", Data = pair.Value.Data }); }).ToDictionary(p => p.Key, p => p.Value); var result = new HealthCheckResponse { Status = r.Status.ToString(), TotalDuration = r.TotalDuration.TotalSeconds.ToString() + "s", Results = results }; await c.Response.WriteAsync(JsonConvert.SerializeObject(result)); }; }); } }
DatabaseHealthCheck.cs
public class DatabaseHealthCheck : IHealthCheck { private static readonly string DefaultTestQuery = "SELECT 1"; private string ConnectionString { get; } private string TestQuery { get; } public DatabaseHealthCheck(string connectionString, string testQuery = null) { if (String.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString)); if (!String.IsNullOrEmpty(testQuery)) TestQuery = testQuery; else TestQuery = DefaultTestQuery; ConnectionString = connectionString; } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { var dataSource = Regex.Match(ConnectionString, @"Data Source=([A-Za-z0-9_.]+)", RegexOptions.IgnoreCase).Value; using (var connection = new MySqlConnection(ConnectionString)) { try { await connection.OpenAsync(cancellationToken); var command = connection.CreateCommand(); command.CommandText = TestQuery; await command.ExecuteNonQueryAsync(cancellationToken); return HealthCheckResult.Healthy(dataSource); } catch (Exception e) { return HealthCheckResult.Unhealthy(dataSource, e); } } } }
Let’s use our tests in a continuous delivery pipeline to ensure quality to our software. Whenever our code is delivered to our master branch, we will automatically build, test and deploy it by using azure pipelines.
Here is a simple .yml file that execute the following pipeline steps on the master branch:
trigger:
- master
pool:
vmImage: ubuntu-16.04
name: PipelineName-${Date:yyyyMMdd}$(Rev:.r)
variables:
ASPNETCORE_ENVIRONMENT: 'Production'
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
displayName: 'unit tests'
inputs:
command: test
projects: '**/*UnitTests/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'integration tests'
inputs:
command: test
projects: '**/*IntegrationTests/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish'
inputs:
command: publish
publishWebProjects: True
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: True
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'myWebsiteName'
- task: AzureWebApp@1
displayName: 'deploy artifacts'
inputs:
azureSubscription: ${{ parameters.azureSubscription }}
appType: webApp
appName: 'yourWebsiteName'
package: $(System.ArtifactsDirectory)/**/*.zip
- task: DotNetCoreCLI@2
displayName: 'health checks'
inputs:
command: test
projects: '**/*HealthChecks/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'functional tests'
inputs:
command: test
projects: '**/*FunctionalTests/*.csproj'
This .yml is already working, you can just create a new pipeline on azure and add this code to run your pipeline!
The tests on this article are fairly simple, this is how a test should be. In order to detect errors as soon as possible in our code, we also have fail fast, that means we need to have simple tests covering our application functionality.
I advocate that every good developer test their code to ensure quality. There is no “I didn’t have time to test”, testing is proven to reduce problems, with no problems there is no extra hour and with no extra hour your company spend less money.
More tests = Less problems = Less money spent in the project
Featured Image by Karl Pawlowicz on Unsplash.