Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSHARP-5338: Improve integration test performance by test fixtures #1332

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions tests/MongoDB.Driver.TestHelpers/IntegrationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

namespace MongoDB.Driver.Tests
{
[IntegrationTest]
public abstract class IntegrationTest<TFixture> : IClassFixture<TFixture>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth inheriting for LoggableTestClass, to have logging and timeouts for all.
Ideally all test would derive from LoggableTestClass.

Copy link
Member Author

@sanych-sun sanych-sun Jun 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntegrationTest is now inherited from LoggableTestClass. Let's discuss offline if we need some extra functionality around.

where TFixture : MongoDatabaseFixture
{
private readonly TFixture _fixture;
BorisDog marked this conversation as resolved.
Show resolved Hide resolved

protected IntegrationTest(TFixture fixture, Action<RequireServer> requireServerCheck = null)
{
_fixture = fixture;
requireServerCheck?.Invoke(RequireServer.Check());
_fixture.BeforeTestCase();
}

public TFixture Fixture => _fixture;
}
}
174 changes: 174 additions & 0 deletions tests/MongoDB.Driver.TestHelpers/LinqIntegrationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
using MongoDB.Driver.Linq.Linq3Implementation;
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators;
using Xunit.Abstractions;

namespace MongoDB.Driver.Tests
{
public abstract class LinqIntegrationTest<TFixture> : IntegrationTest<TFixture>
where TFixture : MongoDatabaseFixture
{
public LinqIntegrationTest(TFixture fixture, Action<RequireServer> requireServerCheck = null)
: base(fixture, requireServerCheck)
{
}

protected void AssertStages(IEnumerable<BsonDocument> stages, params string[] expectedStages)
{
AssertStages(stages, (IEnumerable<string>)expectedStages);
}

protected void AssertStages(IEnumerable<BsonDocument> stages, IEnumerable<string> expectedStages)
{
stages.Should().Equal(expectedStages.Select(json => BsonDocument.Parse(json)));
}

protected static List<BsonDocument> Translate<TDocument, TResult>(IMongoCollection<TDocument> collection, IAggregateFluent<TResult> aggregate) =>
Translate(collection, aggregate, out _);

protected static List<BsonDocument> Translate<TDocument, TResult>(IMongoCollection<TDocument> collection, IAggregateFluent<TResult> aggregate, out IBsonSerializer<TResult> outputSerializer)
{
var pipelineDefinition = ((AggregateFluent<TDocument, TResult>)aggregate).Pipeline;
var documentSerializer = collection.DocumentSerializer;
var translationOptions = aggregate.Options?.TranslationOptions.AddMissingOptionsFrom(collection.Database.Client.Settings.TranslationOptions);
return Translate(pipelineDefinition, documentSerializer, translationOptions, out outputSerializer);
}

// in this overload the collection argument is used only to infer the TDocument type
protected List<BsonDocument> Translate<TDocument, TResult>(IMongoCollection<TDocument> collection, IQueryable<TResult> queryable)
{
return Translate<TDocument, TResult>(queryable);
}

// in this overload the collection argument is used only to infer the TDocument type
protected List<BsonDocument> Translate<TDocument, TResult>(IMongoCollection<TDocument> collection, IQueryable<TResult> queryable, out IBsonSerializer<TResult> outputSerializer)
{
return Translate<TDocument, TResult>(queryable, out outputSerializer);
}

protected static List<BsonDocument> Translate<TResult>(IMongoDatabase database, IAggregateFluent<TResult> aggregate)
{
var pipelineDefinition = ((AggregateFluent<NoPipelineInput, TResult>)aggregate).Pipeline;
var translationOptions = aggregate.Options?.TranslationOptions.AddMissingOptionsFrom(database.Client.Settings.TranslationOptions);
return Translate(pipelineDefinition, NoPipelineInputSerializer.Instance, translationOptions);
}

// in this overload the database argument is used only to infer the NoPipelineInput type
protected List<BsonDocument> Translate<TResult>(IMongoDatabase database, IQueryable<TResult> queryable)
{
return Translate<NoPipelineInput, TResult>(queryable);
}

protected List<BsonDocument> Translate<TDocument, TResult>(IQueryable<TResult> queryable)
{
return Translate<TDocument, TResult>(queryable, out _);
}

protected List<BsonDocument> Translate<TDocument, TResult>(IQueryable<TResult> queryable, out IBsonSerializer<TResult> outputSerializer)
{
var provider = (MongoQueryProvider<TDocument>)queryable.Provider;
var translationOptions = provider.GetTranslationOptions();
var executableQuery = ExpressionToExecutableQueryTranslator.Translate<TDocument, TResult>(provider, queryable.Expression, translationOptions);
var stages = executableQuery.Pipeline.Ast.Stages;
outputSerializer = (IBsonSerializer<TResult>)executableQuery.Pipeline.OutputSerializer;
return stages.Select(s => s.Render().AsBsonDocument).ToList();
}

protected static List<BsonDocument> Translate<TDocument, TResult>(
PipelineDefinition<TDocument, TResult> pipelineDefinition,
IBsonSerializer<TDocument> documentSerializer,
ExpressionTranslationOptions translationOptions) =>
Translate(pipelineDefinition, documentSerializer, translationOptions, out _);

protected static List<BsonDocument> Translate<TDocument, TResult>(
PipelineDefinition<TDocument, TResult> pipelineDefinition,
IBsonSerializer<TDocument> documentSerializer,
ExpressionTranslationOptions translationOptions,
out IBsonSerializer<TResult> outputSerializer)
{
var serializerRegistry = BsonSerializer.SerializerRegistry;
documentSerializer ??= serializerRegistry.GetSerializer<TDocument>();
var renderedPipeline = pipelineDefinition.Render(new(documentSerializer, serializerRegistry, translationOptions: translationOptions));
outputSerializer = renderedPipeline.OutputSerializer;
return renderedPipeline.Documents.ToList();
}

protected BsonDocument Translate<TDocument>(IMongoCollection<TDocument> collection, FilterDefinition<TDocument> filterDefinition)
{
var documentSerializer = collection.DocumentSerializer;
var serializerRegistry = BsonSerializer.SerializerRegistry;
var translationOptions = collection.Database.Client.Settings.TranslationOptions;
return filterDefinition.Render(new(documentSerializer, serializerRegistry, translationOptions: translationOptions));
}

protected BsonDocument TranslateFilter<TDocument>(IMongoCollection<TDocument> collection, FilterDefinition<TDocument> filter)
{
var documentSerializer = collection.DocumentSerializer;
var serializerRegistry = BsonSerializer.SerializerRegistry;
var translationOptions = collection.Database.Client.Settings.TranslationOptions;
return filter.Render(new(documentSerializer, serializerRegistry, translationOptions: translationOptions));
}

protected BsonDocument TranslateFindFilter<TDocument, TProjection>(IMongoCollection<TDocument> collection, IFindFluent<TDocument, TProjection> find)
{
var filterDefinition = ((FindFluent<TDocument, TProjection>)find).Filter;
var translationOptions = collection.Database.Client.Settings.TranslationOptions;
return filterDefinition.Render(new(collection.DocumentSerializer, BsonSerializer.SerializerRegistry, translationOptions: translationOptions));
}

protected BsonDocument TranslateFindProjection<TDocument, TProjection>(
IMongoCollection<TDocument> collection,
IFindFluent<TDocument, TProjection> find) =>
TranslateFindProjection(collection, find, out _);

protected BsonDocument TranslateFindProjection<TDocument, TProjection>(
IMongoCollection<TDocument> collection,
IFindFluent<TDocument, TProjection> find,
out IBsonSerializer<TProjection> projectionSerializer)
{
var projection = ((FindFluent<TDocument, TProjection>)find).Options.Projection;
var translationOptions = find.Options?.TranslationOptions.AddMissingOptionsFrom(collection.Database.Client.Settings.TranslationOptions);
return TranslateFindProjection(collection, projection, translationOptions, out projectionSerializer);
}

protected BsonDocument TranslateFindProjection<TDocument, TProjection>(
IMongoCollection<TDocument> collection,
ProjectionDefinition<TDocument, TProjection> projection,
ExpressionTranslationOptions translationOptions) =>
TranslateFindProjection(collection, projection, translationOptions, out _);

protected BsonDocument TranslateFindProjection<TDocument, TProjection>(
IMongoCollection<TDocument> collection,
ProjectionDefinition<TDocument, TProjection> projection,
ExpressionTranslationOptions translationOptions,
out IBsonSerializer<TProjection> projectionSerializer)
{
var documentSerializer = collection.DocumentSerializer;
var serializerRegistry = BsonSerializer.SerializerRegistry;
var renderedProjection = projection.Render(new(documentSerializer, serializerRegistry, translationOptions: translationOptions, renderForFind: true));
projectionSerializer = renderedProjection.ProjectionSerializer;
return renderedProjection.Document;
}
}
}
75 changes: 75 additions & 0 deletions tests/MongoDB.Driver.TestHelpers/MongoCollectionFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using MongoDB.Driver.Tests;

namespace MongoDB.Driver.TestHelpers
{
public abstract class MongoCollectionFixture<TDocument> : MongoDatabaseFixture
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add:

private readonly Lazy<IMongoCollection<TDocument>> _collection;

private readonly Lazy<IMongoCollection<TDocument>> _collection;
private bool _dataInitialized;

protected MongoCollectionFixture()
{
_collection = new Lazy<IMongoCollection<TDocument>>(CreateCollection);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add:

public IMongoCollection<TDocument> Collection => _collection.Value;

public IMongoCollection<TDocument> Collection => _collection.Value;

protected abstract IEnumerable<TDocument> InitialData { get; }

public virtual bool InitializeDataBeforeEachTestCase => false;

protected override void InitializeTestCase()
{
if (InitializeDataBeforeEachTestCase || !_dataInitialized)
{
Collection.Database.DropCollection(Collection.CollectionNamespace.CollectionName);

if (InitialData == null)
{
Collection.Database.CreateCollection(Collection.CollectionNamespace.CollectionName);
}
else
{
Collection.InsertMany(InitialData);
}

_dataInitialized = true;
}
}

protected virtual string GetCollectionName()
{
var currentType = GetType();
var result = currentType.DeclaringType?.Name;

if (string.IsNullOrEmpty(result))
{
throw new InvalidOperationException("Cannot resolve the collection name. Try to override GetCollectionName for custom collection name resolution.");
}

return result;
}

private IMongoCollection<TDocument> CreateCollection()
{
return CreateCollection<TDocument>(GetCollectionName());
}
}
}
90 changes: 90 additions & 0 deletions tests/MongoDB.Driver.TestHelpers/MongoDatabaseFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;

namespace MongoDB.Driver.Tests
{
public class MongoDatabaseFixture : IDisposable
{
private static readonly string __timeStamp = DateTime.Now.ToString("MMddHHmm");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add:

    private readonly Lazy<IMongoClient> _client;
    private readonly Lazy<IMongoDatabase> _database;

private readonly Lazy<IMongoClient> _client;
private readonly Lazy<IMongoDatabase> _database;
private readonly string _databaseName = $"CSTests{__timeStamp}";
private bool _fixtureInialized;
private readonly HashSet<string> _usedCollections = new();

public MongoDatabaseFixture()
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add:

        _client = new Lazy<IMongoClient>(CreateClient);
        _database = new Lazy<IMongoDatabase>(CreateDatabase);

_client = new Lazy<IMongoClient>(CreateClient);
_database = new Lazy<IMongoDatabase>(CreateDatabase);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add:

    public IMongoClient Client => _client.Value;
    public IMongoDatabase Database => _database.Value;

public IMongoClient Client => _client.Value;
public IMongoDatabase Database => _database.Value;

public virtual void Dispose()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider refactoring to:

    public virtual void Dispose()
    {
        var client = _client.IsValueCreated ? _client.Value : null;
        var database = _database.IsValueCreated ? _database.Value : null;

        if (database != null)
        {
            foreach (var collection in _usedCollections)
            {
                database.DropCollection(collection);
            }
        }

        client?.Dispose();
    }

{
var database = _database.IsValueCreated ? _database.Value : null;
if (database != null)
{
foreach (var collection in _usedCollections)
{
database.DropCollection(collection);
}
}
}

protected IMongoCollection<TDocument> CreateCollection<TDocument>(string collectionName)
{
if (string.IsNullOrEmpty(collectionName))
{
throw new ArgumentException($"{nameof(collectionName)} should be non-empty string.", nameof(collectionName));
}

Database.DropCollection(collectionName);
_usedCollections.Add(collectionName);

return Database.GetCollection<TDocument>(collectionName);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add value factory methods for Lazy _client and _database

    protected virtual IMongoClient CreateClient()
    {
        var clientSettings = DriverTestConfiguration.GetClientSettings();
        ConfigureMongoClientSettings(clientSettings);
        return new MongoClient(clientSettings);
    }

    protected virtual IMongoDatabase CreateDatabase()
    {
        return Client.GetDatabase(_databaseName);
    }

protected virtual IMongoClient CreateClient()
=> DriverTestConfiguration.Client;

protected virtual IMongoDatabase CreateDatabase()
{
return Client.GetDatabase(_databaseName);
}

internal void BeforeTestCase()
{
if (!_fixtureInialized)
{
InitializeFixture();
_fixtureInialized = true;
}

InitializeTestCase();
}

protected virtual void InitializeFixture()
Copy link
Member Author

@sanych-sun sanych-sun Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably have to add some inline documentation here, but the idea was: InitializeFixture method is being executed only once per fixture lifetime (per test class in most cases) when InitializeTestCase is being executed for each test, before the test execution. So combining this two methods it's easy to implement custom initialization logic depending on nature of test collection.

{}

protected virtual void InitializeTestCase()
{}
}
}
Loading