Skip to content

Integration Tests

IOF edited this page Nov 28, 2025 · 3 revisions

Testcontainers

BaseIntegrationTest

public abstract class BaseIntegrationTest : IClassFixture<IntegrationTestApplicationFactory>
{
    protected readonly ISender Sender;
    protected readonly IMediator Mediator;
    protected readonly ModuleBankAppContext ModuleBankAppContext;

    protected BaseIntegrationTest(IntegrationTestApplicationFactory factory)
    {
        var scope = factory.Services.CreateScope();
        Mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
        Sender = scope.ServiceProvider.GetRequiredService<ISender>();
        ModuleBankAppContext = scope.ServiceProvider.GetRequiredService<ModuleBankAppContext>();
    }
}
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Testcontainers.PostgreSql;
using Xunit;

public class GameRepositoryIntegrationTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgreSqlContainer;
    private TicTacToeContext? _dbContext;
    private GameAsyncRepository? _repository;
    private Mock<ILogger<GameAsyncRepository>> _loggerMock;

    public GameRepositoryIntegrationTests()
    {
        _postgreSqlContainer = new PostgreSqlBuilder()
            .WithImage("postgres:latest")
            .WithDatabase("testdb")
            .WithUsername("postgres")
            .WithPassword("postgres")
            .Build();

        _loggerMock = new Mock<ILogger<GameAsyncRepository>>();
    }

    public async Task InitializeAsync()
    {
        await _postgreSqlContainer.StartAsync();

        var options = new DbContextOptionsBuilder<TicTacToeContext>()
            .UseNpgsql(_postgreSqlContainer.GetConnectionString())
            .Options;

        _dbContext = new TicTacToeContext(options);
        await _dbContext.Database.EnsureCreatedAsync();

        // Заполняем тестовыми данными


        for (int i = 0; i < 4; i++)
        {
            var game = new Game
            {
                Id = Guid.NewGuid(),
                Status = StatusGame.Active,
                Board = new string?[][]
                {
                },
                Result = ResultGame.None,
                CreatedAt = default,
                CurrentMove = Player.X,
                CurrentStep = 0
            };
            _dbContext.Games.Add(game);
        }

        await _dbContext.SaveChangesAsync();


        _repository = new GameAsyncRepository(_dbContext, _loggerMock.Object);
    }

    public async Task DisposeAsync()
    {
        await _dbContext!.DisposeAsync();
        await _postgreSqlContainer.DisposeAsync();
    }

    [Fact]
    public async Task GetGamesAsync_ShouldReturn5GamesFromDatabase()
    {
        // Act
        var games = await _repository!.GetGamesAsync(CancellationToken.None);

        // Assert
        Assert.Equal(5, games.Count());
    }
}

IntegrationTestApplicationFactory

public sealed class IntegrationTestApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
        .WithImage("postgres:latest")
        .WithName("postgres_container_name")
        .WithDatabase("testdb")
        .WithUsername("postgres")
        .WithPassword("root")
        .Build();

    private readonly RabbitMqContainer _rabbitMqContainer = new RabbitMqBuilder()
        .WithImage("rabbitmq:3.12-management")
        .WithName("rabbitmq_container_name")
        .WithHostname("rabbitmq_test")
        .Build();

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();
        await _rabbitMqContainer.StartAsync();
    }

    public new async Task DisposeAsync()
    {
        await _dbContainer.StopAsync();
        await _rabbitMqContainer.StopAsync();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var descriptor = services.SingleOrDefault(
                s => s.ServiceType == typeof(DbContextOptions<ModuleBankAppContext>));

            if (descriptor != null) services.Remove(descriptor);

            services.AddDbContext<ModuleBankAppContext>(options =>
                options.UseNpgsql(_dbContainer.GetConnectionString()));

            var hangfireDescriptor = services.FirstOrDefault(d =>
                d.ServiceType == typeof(JobStorage));
            if (hangfireDescriptor != null)
            {
                services.Remove(hangfireDescriptor);
            }

            services.AddHangfire(configuration => configuration
                .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseRecommendedSerializerSettings()
                .UsePostgreSqlStorage(c => c.UseNpgsqlConnection(_dbContainer.GetConnectionString())));

            services.Configure<EventBusOptions>(o =>
            {
                o.HostName = _rabbitMqContainer.Hostname;
                    //o.Port = _rabbitMqContainer.GetMappedPublicPort(5672);
                o.VirtualHost = "/";
                o.ExchangeName = "account.events";
                o.UserName = "guest";
                o.Password = "guest";
            });

            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ModuleBankAppContext>();
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();
        });
    }
}

Exaple Integration Tests

public class CreateAccountIntegrationTests : BaseIntegrationTest
{
    private readonly ClaimsPrincipal _userWithValidId = new(
        new ClaimsIdentity(
            [new Claim(ClaimTypes.NameIdentifier, "a191ee39-08a7-4ffa-8f53-3c5f0f5f9b1c")]
        )
    );

    private readonly ClaimsPrincipal _userWithoutId = new(new ClaimsIdentity());

    private readonly CreateAccountDto _validAccountDto = new(
        AccountType.Checking,
        "USD",
        1000m,
        null
    );

    public CreateAccountIntegrationTests(IntegrationTestApplicationFactory factory)
        : base(factory) { }

    [Fact]
    public async Task HandleRequest_WithInvalidUser_ReturnsUnauthorized()
    {
        // Act
        var result = await CreateAccountEndpoint.HandleEndpoint(
            _validAccountDto,
            Mediator, // <- теперь настоящий Mediator из DI
            _userWithoutId
        );

        // Assert
        Assert.IsType<UnauthorizedHttpResult>(result);
    }

    [Fact]
    public async Task HandleRequest_WhenMediatorReturnsFailure_ReturnsBadRequest()
    {
        // Здесь мы подсовываем невалидный DTO
        var invalidDto = new CreateAccountDto(
            AccountType.Deposit, // для депозита ставка обязательна
            "USD",
            1000m,
            null
        );

        var result = await CreateAccountEndpoint.HandleEndpoint(
            invalidDto,
            Mediator,
            _userWithValidId
        );

        var badRequest = Assert.IsType<BadRequest<MbResult<Account>>>(result);
        Assert.False(badRequest.Value.IsSuccess);
        // Assert.Contains("Interest rate is required", badRequest.Value.Error);
    }

    [Fact]
    public async Task Handle_ShouldCreateAccount_WhenRequestIsValid()
    {
        // Arrange
        var ownerId = Guid.NewGuid();
        var request = new CreateAccountRequest(_validAccountDto, ownerId);

        // Act
        var result = await Sender.Send(request);

        // Assert
        Assert.True(result.IsSuccess);
        Assert.NotNull(result.Value);

        var account = result.Value;
        Assert.Equal(AccountType.Checking, account.Type);
        Assert.Equal("USD", account.Currency);
        Assert.Equal(1000m, account.Balance);
        Assert.Equal(ownerId, account.OwnerId);

        // Проверим, что запись реально появилась в БД
        var dbAccount = await ModuleBankAppContext.Accounts.FindAsync(account.Id);
        Assert.NotNull(dbAccount);
        Assert.Equal("USD", dbAccount!.Currency);
    }
}

Clone this wiki locally