Skip to content

Commit

Permalink
ci: add continuous integration (#9)
Browse files Browse the repository at this point in the history
* ci: add continuous integration

* test: add more tests

* fix: dbcontext instance lifetime
  • Loading branch information
ArwynFr authored Jan 26, 2024
1 parent 1169085 commit bd43911
Show file tree
Hide file tree
Showing 34 changed files with 500 additions and 91 deletions.
18 changes: 18 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@
"commands": [
"dotnet-outdated"
]
},
"dotnet-sonarscanner": {
"version": "6.0.0",
"commands": [
"dotnet-sonarscanner"
]
},
"dotnet-format": {
"version": "5.1.250801",
"commands": [
"dotnet-format"
]
},
"roslynator.dotnet.cli": {
"version": "0.8.3",
"commands": [
"roslynator"
]
}
}
}
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text eol=crlf
30 changes: 30 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Contributing guidelines

This project welcomes contributions:

**Request for support:**
TBD

**Disclose vulnerability:**
Please [create a new security advisory on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/security/advisories)

**Report malfunctions:**
[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose)

**Suggest a feature:**
[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose)

**Offer some code:**
Please [fork the repository](https://github.com/ArwynFr/dotnet-integration-testing/fork)
and [submit a pull-request](https://github.com/ArwynFr/dotnet-integration-testing/compare)

## Definition of Done

Merging a pull request requires:

- dotnet format passes
- dotnet roslynator analyze passes
- dotnet oudated passes
- markdownlint passes
- SonarCloud QualityGate passes
- Documentation updated
94 changes: 94 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ArwynFr.IntegrationTesting

This library provides utility classes for writing integration tests in
dotnet using `XUnit` and `WebApplicationFactory`.

![Nuget.org](https://img.shields.io/nuget/v/ArwynFr.IntegrationTesting?style=for-the-badge)
![Nuget.org](https://img.shields.io/nuget/dt/ArwynFr.IntegrationTesting?style=for-the-badge)
![GitHub
License](https://img.shields.io/github/license/ArwynFr/dotnet-integration-testing?style=for-the-badge)

## Installation

dotnet add package ArwynFr.IntegrationTesting

## Usage

Read [advanced usage
documentation](https://github.com/ArwynFr/dotnet-integration-testing/blob/main/.github/USAGE.adoc)
for further details.

By default, the lib redirects the tested application logs to XUnit
output, so you get them in the test output in case of failure. It also
overwrites the application configuration with values from user secrets
and environement variables.

public class MyTest : IntegrationTestBase<Program>
{
public MyTest(ITestOutputHelper output) : base(output) { }

[Fact]
public async Task OnTest()
{
// Call system under test
var response = await Client.GetFromJsonAsync<OrderDto>($"/order");

response.Should().HaveValue();
}

// Override a service with fake implementation in the tested app
protected override void ConfigureAppServices(IServiceCollection services)
=> services.AddSingleton<IMyService, FakeService>();
}

### EntityFrameworkCore integration

public class TestBaseDb : IntegrationTestBase<Program, MyDbContext>
{
public TestBaseDb(ITestOutputHelper output) : base(output) { }

[Fact]
public async Task OnTest()
{
// Access the injected dbcontext
var value = await Database.Values
.Where(val => val.Id == 1)
.Select(val => val.Result)
.FirstOrDefaultAsync();

// Call system under test
var result = await Client.GetFromJsonAsync<int>("/api/value/1");

result.Should().Be(value + 1);
}

// Create and drop a database for every test execution
protected override IDatabaseTestStrategy<Context> DatabaseTestStrategy
=> IDatabaseTestStrategy<MyDbContext>.DatabasePerTest;

// Configure EFcore with a random database name
protected override void ConfigureDbContext(DbContextOptionsBuilder builder)
=> builder.UseSqlite($"Data Source={Guid.NewGuid()}.sqlite");
}

## Contributing

This project welcomes contributions:

**Request for support:**
TBD

**Disclose vulnerability:**
Please [create a new security advisory on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/security/advisories)

**Report malfunctions:**
[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose)

**Suggest a feature:**
[Please create a new issue on GitHub](https://github.com/ArwynFr/dotnet-integration-testing/issues/new/choose)

**Offer some code:**
Please [fork the repository](https://github.com/ArwynFr/dotnet-integration-testing/fork)
and [submit a pull-request](https://github.com/ArwynFr/dotnet-integration-testing/compare)
\
[Read our definition of done in contributing guidelines](https://github.com/ArwynFr/dotnet-integration-testing/blob/main/.github/CONTRIBUTING.md)
175 changes: 175 additions & 0 deletions .github/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Advanced usage documentation

This library uses `WebApplicationFactory` for integration testing.
Please read [Microsoft’s paper on integration
testing](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0)
for details on how this library works.

## Definitions

**Tested application**
A dotnet application that you want to test. Also known as the *system
under test* (SUT).

**Entrypoint**
A specific class of the tested application that is run when the
application start. For top-level statement applications this is the
`Program` class.

**Test project**
A XUnit test project that implements tests and ensures that the tested
application behaves as expected.

## Basic integration test

Simply extend the `IntegrationTestBase<TEntryPoint>` class and provide
the entrypoint class you want to test:

public class MyTest : IntegrationTestBase<Program>
{
public MyTest(ITestOutputHelper output) : base(output) { }
}

## Arranging the tested application

You can override the following methods to customize the tested
application:

ConfigureAppConfiguration
Change the sources for the app configuration. By default, overrides the
application configuration sources with the following (lower overrides
higher):

- `appsettings.json` file
- User secrets associated with the assembly that contains the entrypoint
- User secrets associated with the assembly that contains the test class
- Environment variables

ConfigureAppLogging
Change the app logging configuration. By default, redirects all logs
(all namespaces and all levels) to the XUnit output helper. If the test
fails, you get all the tested application logs in the test output.

ConfigureAppServices
Override dependency injection services of the tested application.

If your test code and the tested application need to access the same
instance of a service, you need to inject this instance using a
Singleton lifetime. Accessing the DI container outside of the client
call will return an unscoped provider as scopes are created and disposed
by the framework for each request.

// Override a service with custom implementation in the tested app
protected override void ConfigureAppServices(IServiceCollection services)
=> services.AddSingleton<IMyService, FakeService>();

[Fact]
public async Task OnTest()
{
var expected = 3;

// Access the injected service from the test code
var service = Services.GetRequiredService<IMyService>();
service.SetValue(expected);

var response = await Client.GetFromJsonAsync<int>($"/value");

response.Should().Be(expected);
}

## Accessing the tested application

Client
You can access the tested application using the `Client` property which
returns a pseudo `HttpClient` created by `WebApplicationFactory`. You
access your application like a client application would:

<!-- -->

[Fact]
public async Task OnTest()
{
var response = await Client.GetFromJsonAsync<OrderDto>($"/order");
response.Should().HaveValue();
}

Configuration
This property grants you acces to the `IConfiguration` values that are
currently available to the tested application.

Services
This property grants you access to the DI service provider of the tested
application.

If your test code and the tested application need to access the same
instance of a service, you need to inject this instance using a
Singleton lifetime. Accessing the DI container outside of the client
call will return an unscoped provider as scopes are created and disposed
by the framework for each request.

## Extending the behavior of the test class

You can override the following methods to run code before and after each
test:

InitializeAsync
Code executed before the execution of each test of the class.

DisposeAsync
Code executed after the execution of each test of the class.

## EntityFrameworkCore integration

The library provides specific support for efcore. You can achieve this
integration by extending the
`IntegrationTestBase<TEntryPoint, TContext>` class instead of the one
that only uses the entrypoint.

You will need to override the abstract `ConfigureDbContext` method to
tell the dependency injection library how to configure your context. A
context instance will be generated per test and injected in your target
app as a singleton. You can access the same context instance in your
test through the `Database` property.

protected override void ConfigureDbContext(DbContextOptionsBuilder builder)
=> builder.UseSqlite($"Data Source=test.sqlite");

The base class also exposes a `DatabaseTestStrategy` property that
allows you to customize the test behavior regarding the database. You
can write your own implementation which requires you to write specific
code that will run before and after each test to set your database.

The library comes with 3 standard behaviors:

`IDatabaseTestStrategy<TContext>.Default`
By default the library simply instantates the context and disposes the
instance after the test execution.

`IDatabaseTestStrategy<TContext>.Transaction`
This behavior will execute each test in a separate transaction. This can
be used to prevent write operations to change the contents of the
database. Obviously requires a database engine that supports
transactions.

`IDatabaseTestStrategy<TContext>.DatabasePerTest`
This behavior creates a fresh database before test execution and drops
it afterwards. It also applies migrations if any are found, otherwise it
will use `EnsureCreated` (read [Create and Drop
APIs](https://learn.microsoft.com/en-us/ef/core/managing-schemas/ensure-created)
to understand how this might affect your test results). This allows test
parallelization when transaction isolation is not sufficient or
unavailable. You must combine this behavior with a random name
interpolation in the connection string to run each test on it’s own
database in parallel. Otherwise the tests will try to access the same
database concurrently and will fail to drop it while other tests are
running:

<!-- -->

protected override IDatabaseTestStrategy<Context> DatabaseTestStrategy
=> IDatabaseTestStrategy<Context>.DatabasePerTest;

protected override void ConfigureDbContext(DbContextOptionsBuilder builder)
=> builder.UseSqlite($"Data Source={Guid.NewGuid()}.sqlite");

This beahvior WILL drop your database after each test !
Loading

0 comments on commit bd43911

Please sign in to comment.