init k8s guide
This commit is contained in:
@ -30,20 +30,11 @@ services:
|
|||||||
POSTGRES_DB: main
|
POSTGRES_DB: main
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
db_test:
|
|
||||||
image: postgres:15
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: main
|
|
||||||
POSTGRES_PASSWORD: main
|
|
||||||
POSTGRES_DB: main
|
|
||||||
ports:
|
|
||||||
- 54320:5432
|
|
||||||
```
|
```
|
||||||
|
|
||||||
{{< /highlight >}}
|
{{< /highlight >}}
|
||||||
|
|
||||||
Here we create 2 PostgreSQL instances, one for local development and one for integration testing. Launch them with `docker compose up -d` and check they are both running with `docker ps`.
|
Launch it with `docker compose up -d` and check database running with `docker ps`.
|
||||||
|
|
||||||
Time to create basic code that list plenty of articles from an API endpoint. Go back to `kuberocks-demo` and create a new separate project dedicated to app logic:
|
Time to create basic code that list plenty of articles from an API endpoint. Go back to `kuberocks-demo` and create a new separate project dedicated to app logic:
|
||||||
|
|
||||||
@ -83,8 +74,8 @@ public class Article
|
|||||||
public required string Description { get; set; }
|
public required string Description { get; set; }
|
||||||
public required string Body { get; set; }
|
public required string Body { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public ICollection<Comment> Comments { get; } = new List<Comment>();
|
public ICollection<Comment> Comments { get; } = new List<Comment>();
|
||||||
}
|
}
|
||||||
@ -106,7 +97,7 @@ public class Comment
|
|||||||
|
|
||||||
public required string Body { get; set; }
|
public required string Body { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -506,7 +497,7 @@ public class ArticlesController
|
|||||||
public ArticlesResponse Get([FromQuery] int page = 1, [FromQuery] int size = 10)
|
public ArticlesResponse Get([FromQuery] int page = 1, [FromQuery] int size = 10)
|
||||||
{
|
{
|
||||||
var articles = _context.Articles
|
var articles = _context.Articles
|
||||||
.OrderByDescending(a => a.CreatedAt)
|
.OrderByDescending(a => a.Id)
|
||||||
.Skip((page - 1) * size)
|
.Skip((page - 1) * size)
|
||||||
.Take(size)
|
.Take(size)
|
||||||
.ProjectToType<ArticleListDto>();
|
.ProjectToType<ArticleListDto>();
|
||||||
@ -521,7 +512,7 @@ public class ArticlesController
|
|||||||
{
|
{
|
||||||
var article = _context.Articles
|
var article = _context.Articles
|
||||||
.Include(a => a.Author)
|
.Include(a => a.Author)
|
||||||
.Include(a => a.Comments.OrderByDescending(c => c.CreatedAt))
|
.Include(a => a.Comments.OrderByDescending(c => c.Id))
|
||||||
.ThenInclude(c => c.Author)
|
.ThenInclude(c => c.Author)
|
||||||
.FirstOrDefault(a => a.Slug == slug);
|
.FirstOrDefault(a => a.Slug == slug);
|
||||||
|
|
||||||
|
@ -322,14 +322,320 @@ Let's cover the feature testing by calling the API against a real database. This
|
|||||||
|
|
||||||
### xUnit
|
### xUnit
|
||||||
|
|
||||||
|
First add a dedicated database for test in the docker compose file as we won't interfere with the development database:
|
||||||
|
|
||||||
|
{{< highlight host="kuberocks-demo" file="docker-compose.yml" >}}
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
#...
|
||||||
|
|
||||||
|
db_test:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: main
|
||||||
|
POSTGRES_PASSWORD: main
|
||||||
|
POSTGRES_DB: main
|
||||||
|
ports:
|
||||||
|
- 54320:5432
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
Expose the startup service of minimal API:
|
||||||
|
|
||||||
|
{{< highlight host="kuberocks-demo" file="src/KubeRocks.WebApi/Program.cs" >}}
|
||||||
|
|
||||||
|
```cs
|
||||||
|
#...
|
||||||
|
public partial class Program { }
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
Then add a testing JSON environment file for accessing our database `db_test` from the docker-compose.yml:
|
||||||
|
|
||||||
|
{{< highlight host="kuberocks-demo" file="src/KubeRocks.WebApi/appsettings.Testing.json" >}}
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=localhost;Port=54320;User Id=main;Password=main;Database=main;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
Now the test project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dotnet new xunit -o tests/KubeRocks.FeatureTests
|
dotnet new xunit -o tests/KubeRocks.FeatureTests
|
||||||
|
dotnet sln add tests/KubeRocks.FeatureTests
|
||||||
dotnet add tests/KubeRocks.FeatureTests reference src/KubeRocks.WebApi
|
dotnet add tests/KubeRocks.FeatureTests reference src/KubeRocks.WebApi
|
||||||
|
dotnet add tests/KubeRocks.FeatureTests package Microsoft.AspNetCore.Mvc.Testing
|
||||||
dotnet add tests/KubeRocks.FeatureTests package Respawn
|
dotnet add tests/KubeRocks.FeatureTests package Respawn
|
||||||
dotnet add tests/KubeRocks.FeatureTests package FluentAssertions
|
dotnet add tests/KubeRocks.FeatureTests package FluentAssertions
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code Coverage
|
The `WebApplicationFactory` that will use our testing environment:
|
||||||
|
|
||||||
|
{{< highlight host="kuberocks-demo" file="tests/KubeRocks.FeatureTests/KubeRocksApiFactory.cs" >}}
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace KubeRocks.FeatureTests;
|
||||||
|
|
||||||
|
public class KubeRocksApiFactory : WebApplicationFactory<Program>
|
||||||
|
{
|
||||||
|
protected override IHost CreateHost(IHostBuilder builder)
|
||||||
|
{
|
||||||
|
builder.UseEnvironment("Testing");
|
||||||
|
|
||||||
|
return base.CreateHost(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
The base test class for all test classes that manages database cleanup thanks to `Respawn`:
|
||||||
|
|
||||||
|
{{< highlight host="kuberocks-demo" file="tests/KubeRocks.FeatureTests/TestBase.cs" >}}
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using KubeRocks.Application.Contexts;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
using Respawn;
|
||||||
|
using Respawn.Graph;
|
||||||
|
|
||||||
|
namespace KubeRocks.FeatureTests;
|
||||||
|
|
||||||
|
[Collection("Sequencial")]
|
||||||
|
public class TestBase : IClassFixture<KubeRocksApiFactory>, IAsyncLifetime
|
||||||
|
{
|
||||||
|
protected readonly KubeRocksApiFactory _factory;
|
||||||
|
|
||||||
|
protected TestBase(KubeRocksApiFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshDatabase()
|
||||||
|
{
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
|
||||||
|
using var conn = new NpgsqlConnection(
|
||||||
|
scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.GetConnectionString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var respawner = await Respawner.CreateAsync(conn, new RespawnerOptions
|
||||||
|
{
|
||||||
|
TablesToIgnore = new Table[] { "__EFMigrationsHistory" },
|
||||||
|
DbAdapter = DbAdapter.Postgres
|
||||||
|
});
|
||||||
|
|
||||||
|
await respawner.ResetAsync(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitializeAsync()
|
||||||
|
{
|
||||||
|
return RefreshDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
Note the `Collection` attribute that will force the test classes to run sequentially, required as we will use the same database for all tests.
|
||||||
|
|
||||||
|
Finally, the tests for the 2 endpoints of our articles controller:
|
||||||
|
|
||||||
|
{{< highlight host="kuberocks-demo" file="tests/KubeRocks.FeatureTests/Articles/ArticlesListTests.cs" >}}
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
using FluentAssertions;
|
||||||
|
|
||||||
|
using KubeRocks.Application.Contexts;
|
||||||
|
using KubeRocks.Application.Entities;
|
||||||
|
using KubeRocks.WebApi.Models;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
using static KubeRocks.WebApi.Controllers.ArticlesController;
|
||||||
|
|
||||||
|
namespace KubeRocks.FeatureTests.Articles;
|
||||||
|
|
||||||
|
public class ArticlesListTests : TestBase
|
||||||
|
{
|
||||||
|
public ArticlesListTests(KubeRocksApiFactory factory) : base(factory) { }
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Can_Paginate_Articles()
|
||||||
|
{
|
||||||
|
using (var scope = _factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
|
var user = db.Users.Add(new User
|
||||||
|
{
|
||||||
|
Name = "John Doe",
|
||||||
|
Email = "john.doe@email.com"
|
||||||
|
});
|
||||||
|
|
||||||
|
db.Articles.AddRange(Enumerable.Range(1, 50).Select(i => new Article
|
||||||
|
{
|
||||||
|
Title = $"Test Title {i}",
|
||||||
|
Slug = $"test-title-{i}",
|
||||||
|
Description = "Test Description",
|
||||||
|
Body = "Test Body",
|
||||||
|
Author = user.Entity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _factory.CreateClient().GetAsync("/api/Articles?page=1&size=20");
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var body = (await response.Content.ReadFromJsonAsync<ArticlesResponse>())!;
|
||||||
|
|
||||||
|
body.Articles.Count().Should().Be(20);
|
||||||
|
body.ArticlesCount.Should().Be(50);
|
||||||
|
|
||||||
|
body.Articles.First().Should().BeEquivalentTo(new
|
||||||
|
{
|
||||||
|
Title = "Test Title 50",
|
||||||
|
Description = "Test Description",
|
||||||
|
Body = "Test Body",
|
||||||
|
Author = new
|
||||||
|
{
|
||||||
|
Name = "John Doe"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Can_Get_Article()
|
||||||
|
{
|
||||||
|
using (var scope = _factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
|
db.Articles.Add(new Article
|
||||||
|
{
|
||||||
|
Title = $"Test Title",
|
||||||
|
Slug = $"test-title",
|
||||||
|
Description = "Test Description",
|
||||||
|
Body = "Test Body",
|
||||||
|
Author = new User
|
||||||
|
{
|
||||||
|
Name = "John Doe",
|
||||||
|
Email = "john.doe@email.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _factory.CreateClient().GetAsync($"/api/Articles/test-title");
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var body = (await response.Content.ReadFromJsonAsync<ArticleDto>())!;
|
||||||
|
|
||||||
|
body.Should().BeEquivalentTo(new
|
||||||
|
{
|
||||||
|
Title = "Test Title",
|
||||||
|
Description = "Test Description",
|
||||||
|
Body = "Test Body",
|
||||||
|
Author = new
|
||||||
|
{
|
||||||
|
Name = "John Doe"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
Ensure all tests passes with `dotnet test`.
|
||||||
|
|
||||||
|
### CI tests & code coverage
|
||||||
|
|
||||||
|
Now we need to integrate the tests in our CI pipeline. As we testing with a real database, create a new `demo_test` database through pgAdmin with basic `test` / `test` credentials.
|
||||||
|
|
||||||
|
{{< alert >}}
|
||||||
|
In real world scenario, you should use a dedicated database for testing, and not the same as production.
|
||||||
|
{{< /alert >}}
|
||||||
|
|
||||||
|
Let's edit the pipeline accordingly for tests:
|
||||||
|
|
||||||
|
{{< highlight host="demo-kube-flux" file="pipelines/demo.yaml" >}}
|
||||||
|
|
||||||
|
```yml
|
||||||
|
#...
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: build
|
||||||
|
plan:
|
||||||
|
#...
|
||||||
|
|
||||||
|
- task: build-source
|
||||||
|
config:
|
||||||
|
#...
|
||||||
|
params:
|
||||||
|
ConnectionStrings__DefaultConnection: "Server=postgres-primary.postgres; Port=5432; User Id=test; Password=test; Database=demo_test"
|
||||||
|
run:
|
||||||
|
path: /bin/sh
|
||||||
|
args:
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
dotnet format --verify-no-changes
|
||||||
|
|
||||||
|
dotnet sonarscanner begin /k:"KubeRocks-Demo" /d:sonar.host.url="((sonarqube.url))" /d:sonar.token="((sonarqube.analysis-token))" /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml
|
||||||
|
dotnet build -c Release
|
||||||
|
dotnet-coverage collect 'dotnet test -c Release --no-restore --no-build --verbosity=normal' -f xml -o 'coverage.xml'
|
||||||
|
dotnet sonarscanner end /d:sonar.token="((sonarqube.analysis-token))"
|
||||||
|
|
||||||
|
dotnet publish src/KubeRocks.WebApi -c Release -o publish --no-restore --no-build
|
||||||
|
|
||||||
|
#...
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< /highlight >}}
|
||||||
|
|
||||||
|
Note as we already include code coverage by using `dotnet-coverage` tool. Don't forget to precise the path of `coverage.xml` to `sonarscanner` CLI too. It's time to push our code with tests or trigger the pipeline manually to test our integration tests.
|
||||||
|
|
||||||
|
If all goes well, you should see the tests results on SonarQube with some coverage done:
|
||||||
|
|
||||||
|
[](sonarqube-tests.png)
|
||||||
|
|
||||||
|
Coverage detail:
|
||||||
|
|
||||||
|
[](sonarqube-coverage.png)
|
||||||
|
|
||||||
|
### Sonar Lint
|
||||||
|
|
||||||
## Load testing
|
## Load testing
|
||||||
|
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 389 KiB |
Binary file not shown.
After Width: | Height: | Size: 386 KiB |
Reference in New Issue
Block a user