Skip to content

MongoDB Atlas Local

MongoDB Atlas Local runs a local Atlas deployment in a single container. Next to MongoDB, it includes the Atlas Search process, so features like Atlas Search ($search) and Atlas Vector Search ($vectorSearch) can be tested without an Atlas cluster.

Add the following dependency to your project file:

NuGet
1
dotnet add package Testcontainers.MongoDbAtlasLocal

You can start a MongoDB Atlas Local container instance from any .NET application. Here, we create different container instances and pass them to the base test class. This allows us to test different configurations. Authentication is disabled by default. Set a username and a password to enable it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[UsedImplicitly]
public sealed class MongoDbAtlasLocalDefaultConfiguration : MongoDbAtlasLocalContainerTest
{
    public MongoDbAtlasLocalDefaultConfiguration()
        : base(new MongoDbAtlasLocalBuilder(TestSession.GetImageFromDockerfile()).Build())
    {
    }
}

[UsedImplicitly]
public sealed class MongoDbAtlasLocalAuthConfiguration : MongoDbAtlasLocalContainerTest
{
    public MongoDbAtlasLocalAuthConfiguration()
        : base(new MongoDbAtlasLocalBuilder(TestSession.GetImageFromDockerfile()).WithUsername("mongo").WithPassword("mongo").Build())
    {
    }
}

This example uses xUnit.net's IAsyncLifetime interface to manage the lifecycle of the container. The container is started in the InitializeAsync method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the DisposeAsync method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public async ValueTask InitializeAsync()
{
    await _mongoDbAtlasLocalContainer.StartAsync()
        .ConfigureAwait(false);
}

public async ValueTask DisposeAsync()
{
    await DisposeAsyncCore()
        .ConfigureAwait(false);

    GC.SuppressFinalize(this);
}

[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public void ConnectionStateReturnsOpen()
{
    // Given
    var client = new MongoClient(_mongoDbAtlasLocalContainer.GetConnectionString());

    // When
    using var databases = client.ListDatabases(TestContext.Current.CancellationToken);

    // Then
    Assert.Contains(databases.ToEnumerable(TestContext.Current.CancellationToken), database => database.TryGetValue("name", out var name) && "admin".Equals(name.AsString));
    Assert.Equal(_mongoDbAtlasLocalContainer.GetConnectionString(), _mongoDbAtlasLocalContainer.GetConnectionString(ConnectionMode.Host));
}

[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task AtlasSearchReturnsMatchingDocuments()
{
    // Given
    const string indexName = "default";

    var client = new MongoClient(_mongoDbAtlasLocalContainer.GetConnectionString());

    var collection = client.GetDatabase("test").GetCollection<BsonDocument>("movies");

    await collection.InsertManyAsync(new[] { new BsonDocument("title", "The Matrix"), new BsonDocument("title", "Back to the Future"), new BsonDocument("title", "The Matrix Reloaded") }, cancellationToken: TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    await collection.SearchIndexes.CreateOneAsync(new CreateSearchIndexModel(indexName, new BsonDocument("mappings", new BsonDocument("dynamic", true))), TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    // When
    await AtlasSearch.WaitUntilSearchIndexIsQueryableAsync(collection, indexName, TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    var titles = await AtlasSearch.WaitUntilSearchReturnsAsync(collection, indexName, Builders<BsonDocument>.Search.Text("title", "matrix"), 2, TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    // Then
    Assert.Equal(new[] { "The Matrix", "The Matrix Reloaded" }, titles.OrderBy(title => title));
}

[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task ExecScriptReturnsSuccessful()
{
    // Given
    const string scriptContent = "printjson(db.adminCommand({listDatabases:1,nameOnly:true,filter:{\"name\":/^admin/}}));";

    // When
    var execResult = await _mongoDbAtlasLocalContainer.ExecScriptAsync(scriptContent, TestContext.Current.CancellationToken)
        .ConfigureAwait(true);

    // Then
    Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr);
    Assert.Empty(execResult.Stderr);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace Testcontainers.MongoDbAtlasLocal;

internal static class AtlasSearch
{
    private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(250);

    private static readonly TimeSpan PollTimeout = TimeSpan.FromMinutes(1);

    public static async Task WaitUntilSearchIndexIsQueryableAsync(IMongoCollection<BsonDocument> collection, string indexName, CancellationToken ct)
    {
        // Atlas Search builds the index asynchronously, it becomes queryable shortly after it has been created.
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        timeoutCts.CancelAfter(PollTimeout);

        while (true)
        {
            using var cursor = await collection.SearchIndexes.ListAsync(indexName, cancellationToken: timeoutCts.Token)
                .ConfigureAwait(false);

            var searchIndexes = await cursor.ToListAsync(timeoutCts.Token)
                .ConfigureAwait(false);

            if (searchIndexes.Any(searchIndex => searchIndex.TryGetValue("queryable", out var queryable) && queryable.ToBoolean()))
            {
                return;
            }

            await Task.Delay(PollInterval, timeoutCts.Token)
                .ConfigureAwait(false);
        }
    }

    public static async Task<string[]> WaitUntilSearchReturnsAsync(IMongoCollection<BsonDocument> collection, string indexName, SearchDefinition<BsonDocument> searchDefinition, int expectedCount, CancellationToken ct)
    {
        // mongot replicates documents from mongod asynchronously, a queryable index may not contain all documents yet.
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        timeoutCts.CancelAfter(PollTimeout);

        while (true)
        {
            var documents = await collection.Aggregate()
                .Search(searchDefinition, indexName: indexName)
                .ToListAsync(timeoutCts.Token)
                .ConfigureAwait(false);

            if (documents.Count >= expectedCount)
            {
                return documents.Select(document => document["title"].AsString).ToArray();
            }

            await Task.Delay(PollInterval, timeoutCts.Token)
                .ConfigureAwait(false);
        }
    }
}

The test example uses the following NuGet dependencies:

1
2
3
4
5
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="xunit.runner.visualstudio"/>
<PackageReference Include="xunit.v3"/>
<PackageReference Include="MongoDB.Driver"/>

To execute the tests, use the command dotnet test from a terminal.

Tip

For the complete source code of this example and additional information, please refer to our test projects.

Note

Atlas Search builds indexes asynchronously. A search index becomes queryable shortly after it has been created, and newly written documents become searchable shortly after they have been written. Wait until the index reports queryable: true before running search queries, and retry a query until it returns the documents you expect, as the Atlas Search helper above does.

Seeding the deployment

Init scripts seed the deployment on its first start. WithInitScript(string) copies a script file from the test host, and WithInitScriptContent(string, string) creates one from a string. JavaScript (.js) scripts run in mongosh against the database set with WithInitDatabase(string) (default test), and shell (.sh) scripts run in bash. The container silently skips files with any other extension, so the builder rejects them. Scripts run in alphabetical order of their file names and finish before the container is reported ready. They can also create Atlas Search indexes. A restarted or reused container keeps its data and does not run the scripts again.

1
2
3
4
5
6
7
8
private readonly MongoDbAtlasLocalContainer _mongoDbAtlasLocalContainer = new MongoDbAtlasLocalBuilder(TestSession.GetImageFromDockerfile())
    .WithUsername("mongo")
    .WithPassword("mongo")
    .WithInitDatabase(Database)
    .WithInitScript("Seed/01-movies.js")
    .WithInitScriptContent("02-marker.sh", "touch " + SeedMarkerFilePath)
    .WithNoTelemetry()
    .Build();
1
2
3
4
5
6
7
db.movies.insertMany([
  { title: "The Matrix" },
  { title: "Back to the Future" },
  { title: "The Matrix Reloaded" },
]);

db.movies.createSearchIndex("default", { mappings: { dynamic: true } });

Telemetry

The MongoDB Atlas Local image sends telemetry to MongoDB by default. Call WithNoTelemetry() to disable it, as shown in the seed configuration above.