skip to Main Content

I am integration testing a .NET API that connects to MongoDB using the connection string and database name values stored in an app.settings.json file. I am using xUnit and Test Containers to carry out the tests. When the tests are carried out, the environment variable changes and prompts my Program.cs to connect to a test database. I originally wanted to wipe and reseed this test database between each test or carry out some kind of rollback but I could not find a simple way to do this.

I am now trying to instead create a local mock of the test database, however when I build and connect to the API in the tests, it still points to the live test database instead of the local one. I want to be able to change the connection string in an app.settings.json to the connection string that is generated in the test fixture every time the test runs but I am not sure how to do this. I have looked into creating a custom WebFactory and dependency injection but am not sure how to implement this with the code I have so far.

My Program.cs file looks like this


public partial class Program
{
    static void Main(string[] args)
    {

        var builder = WebApplication.CreateBuilder(args);

        bool isTestEnvironment = Helper.IsTestEnvironment();

        builder.Services.Configure<ArtsieDatabaseSettings>(
        builder.Configuration.GetSection(isTestEnvironment ? "ArtsieTestDatabase" : "ArtsieDatabase"));

        builder.Services.AddSingleton<ArtsieService>();

        builder.Services.AddControllers()
            .AddJsonOptions(
                options => options.JsonSerializerOptions.PropertyNamingPolicy = null);

        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "Art API", Description = "Browse some beautiful art", Version = "v1" });
        });

        var app = builder.Build();

        app.UseHttpsRedirection();
        app.MapControllers();

//... rest of swagger documentation and endpoints

        app.Run();


    }
}

My test file looks like this:

namespace art_api.Tests;

using ArtsieApi.Models;
using Microsoft.AspNetCore.Mvc.Testing;
using Newtonsoft.Json;
using System.Net;
using System.Text;

using MongoDB.Driver;
using Testcontainers.MongoDb;
using Xunit;

public class DatabaseFixture : IDisposable
{
    private readonly IMongoDatabase _database;
    private readonly MongoDbContainer _container;

    public DatabaseFixture()
    {

        Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", "true");

        _container = new MongoDbBuilder().Build();

        _container.StartAsync().Wait();

        var connectionString = _container.GetConnectionString();

        var client = new MongoClient(connectionString);
        _database = client.GetDatabase("artsie-test");

        // Seed initial data
        SeedTestData();
    }

    private void SeedTestData()
    {
        var artCollection = _database.GetCollection<List<Art>>("art");
        var commentsCollection = _database.GetCollection<List<Comment>>("comments");
        var usersCollection = _database.GetCollection<List<User>>("users");

    }

    public void Dispose()
    {

        _database.DropCollection("art");
        _database.DropCollection("comments");
        _database.DropCollection("users");

        _container.DisposeAsync().GetAwaiter().GetResult();

        Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", null);

    }
}

public class Endpoints : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _fixture;

    public Endpoints(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }


    [Fact(DisplayName = "200: GET /")]
    public async Task TestRootEndpoint()
    {
        await using var application = new WebApplicationFactory<Program>();
        using var client = application.CreateClient();

        var response = await client.GetAsync("/");
        var content = await response.Content.ReadAsStringAsync();

        Assert.Equal("Hello World!", content);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
//... rest of tests
}

2

Answers


  1. Chosen as BEST ANSWER

    So after a lot of frustration and with the help of Google/YouTube/ChatGPT I found a solution!

    To start with, I created a CustomWebApplicationFactory class that grabs the local connection string generated in my Fixture class and inserts it into a new instance of DatabaseSettings.

    I also fixed the SeedTestData function to populate the local database with the test data stored in some .json files.

    My tests now use CustomWebApplicationFactory when building the application for tests. Pretty sure there is more that can be refactored but I'm glad to have found a solution!

    internal class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        private readonly DatabaseFixture _databaseFixture;
    
        public CustomWebApplicationFactory(DatabaseFixture databaseFixture)
        {
            _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture));
        }
    
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureTestServices(services =>
            {
                services.RemoveAll(typeof(IOptions<ArtsieDatabaseSettings>));
    
                // Access the MongoDbContainer from DatabaseFixture
                var container = _databaseFixture._container;
    
                // Create a new instance of ArtsieDatabaseSettings with test connection string
                var testConnectionString = container.GetConnectionString();
                var testArtsieSettings = new ArtsieDatabaseSettings
                {
                    ConnectionString = testConnectionString,
                    DatabaseName = "artsie-test",
                    ArtCollectionName = "art",
                    CommentsCollectionName = "comments",
                    UsersCollectionName = "users"
                };
    
                // Register the new instance of IOptions<ArtsieDatabaseSettings>
                services.AddSingleton<IOptions<ArtsieDatabaseSettings>>(_ => Options.Create(testArtsieSettings));
            });
        }
    }
    
    public class DatabaseFixture : IDisposable
    {
        private readonly IMongoDatabase _database;
        public MongoDbContainer _container { get; private set; }
    
        public DatabaseFixture()
        {
    
            Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", "true");
    
            // Initialize Testcontainers.MongoDb MongoDB container
            _container = new MongoDbBuilder().Build();
    
            // Start the container
            _container.StartAsync().Wait();
    
            // Get MongoDB connection string
            var connectionString = _container.GetConnectionString();
    
    
            // Connect to the MongoDB database
            var client = new MongoClient(connectionString);
            _database = client.GetDatabase("artsie-test");
    
            // Seed initial data
            SeedTestData();
        }
        private void SeedTestData()
        {
    
            string currentDirectory = AppDomain.CurrentDomain.BaseDirectory;
            string dataFolderPath = Path.Combine(currentDirectory, "data");
    
            string artFilePath = Path.Combine(dataFolderPath, "art.json");
            string commentsFilePath = Path.Combine(dataFolderPath, "comments.json");
            string usersFilePath = Path.Combine(dataFolderPath, "users.json");
    
            // Read JSON file containing test data
            string artJson = File.ReadAllText(artFilePath);
            string commentsJson = File.ReadAllText(commentsFilePath);
            string usersJson = File.ReadAllText(usersFilePath);
    
            // Deserialize JSON into list of objects
            var artData = JsonConvert.DeserializeObject<List<Art>>(artJson);
            var commentsData = JsonConvert.DeserializeObject<List<Comment>>(commentsJson);
            var usersData = JsonConvert.DeserializeObject<List<User>>(usersJson);
            // Implement seeding logic to populate the database with test data
            // For example:
            var artCollection = _database.GetCollection<Art>("art");
            var commentsCollection = _database.GetCollection<Comment>("comments");
            var usersCollection = _database.GetCollection<User>("users");
    
            artCollection.InsertMany(artData);
            commentsCollection.InsertMany(commentsData);
            usersCollection.InsertMany(usersData);
    
        }
    
        public void Dispose()
        {
            // Clean up after tests
            // Drop collections or perform any necessary cleanup operations
            // For example:
            _database.DropCollection("art");
            _database.DropCollection("comments");
            _database.DropCollection("users");
    
            // Dispose of the container
            _container.DisposeAsync().GetAwaiter().GetResult();
    
            Environment.SetEnvironmentVariable("TEST_ENVIRONMENT", null);
    
        }
    }
    
    public class Endpoints : IClassFixture<DatabaseFixture>
    {
        private readonly DatabaseFixture _fixture;
    
        public Endpoints(DatabaseFixture fixture)
        {
            _fixture = fixture;
        }
    
    
        [Fact(DisplayName = "200: GET /")]
        public async Task TestRootEndpoint()
        {
            await using var application = new CustomWebApplicationFactory(_fixture);
            using var client = application.CreateClient();
    
            var response = await client.GetAsync("/");
            var content = await response.Content.ReadAsStringAsync();
    
            Assert.Equal("Hello World!", content);
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }


  2. In case you have IOptionsMonitor, better to:

    services.Configure<ArtsieDatabaseSettings>(t =>
    {
        t.ConnectionString = testConnectionString;
        t.DatabaseName = "artsie-test";
        t.ArtCollectionName = "art";
        t.CommentsCollectionName = "comments";
        t.UsersCollectionName = "users";
    });
    

    Like explained here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search