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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

sanych-sun
Copy link
Member

Most of the integration tests we have currently re-inserting the same data for each run. For the tests that is reading data (like a most LINQ tests) it's inefficient. Proposed changes let us utilize xUnit's Fixture to share the test data for the test collection. It means that Fixture will hold the test data, which will be inserted only one before tests will be run for a test class. Also fixture will be disposed after all tests were run, so we can cleanup the test collection.

I've choose 20 last tests in Jira folder because all of them are integration and refactored to use the approach. On mine local machine I saw like a 4 times improvement for this tests.
Unit-tests-before
Unit-tests-after

@sanych-sun sanych-sun requested a review from a team as a code owner May 27, 2024 23:37
@sanych-sun sanych-sun requested review from JamesKovacs, rstam, BorisDog and adelinowona and removed request for a team May 27, 2024 23:38

protected TFixture Fixture { get; }

protected void AssertStages(IEnumerable<BsonDocument> stages, params string[] expectedStages)
Copy link
Member Author

Choose a reason for hiding this comment

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

All Assert- and Translate- methods were simply copied from existing Linq3IntegrationTest base class (which probably has to be removed)

Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of these Translate methods only apply to LINQ integration tests, does it matter that they are in a more general purpose class now called IntegrationTest when not all integration tests use them?

We could derive Linq3IntegrationTest from IntegrationTest to keep the LINQ specific methods contained.

Or we could just decide it's OK for them to be bundled here. In which case the Linq3IntegrationTest class could be removed once nothing is using it any more.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the idea to derive Linq3IntegrationTest from IntegrationTest. Will check if we can do it now, or should do it as the very last step of the refactoring if there are any conflicts.

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.

I've decided to introduce new LinqIntegrationTest base class.
For 2 reasons:

  1. we will be using the class to test both V3 and V2 LinqProvider (as we mostly doing)
  2. In such way we can gradually update tests and once done - we will remove 'old' base classes.

using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
{
public class CSharp702Tests : Linq3IntegrationTest
public class CSharp702Tests : IntegrationTest<CSharp702Tests.TestDataFixture>
Copy link
Member Author

Choose a reason for hiding this comment

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

The main difference with comparing to the current test class structure:

  1. Test class should be inherited from IntegrationTest base class
  2. For typical test class that uses only 1 set of data we should create TestDataFixture as nested class and provide it as a type parameter for base IntegrationTest
  3. Test class should have ctor to pass the Fixture created by xUnit infrastructure
  4. Test collection is available through the Fixture property
  5. Test data should be moved from GetCollection method to TestDataFixture

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe fix the class name while we're at it? Doesn't match the filename.

Copy link
Member Author

Choose a reason for hiding this comment

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

Class was renamed.

protected TFixture Fixture { get; }

protected void AssertStages(IEnumerable<BsonDocument> stages, params string[] expectedStages)
{
Copy link
Member Author

@sanych-sun sanych-sun May 27, 2024

Choose a reason for hiding this comment

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

BTW, we already have 2 sets of such methods: in Linq3IntegrationTest class and also in Linq3TestHelpers static class. We probably ought to standardize the approach and have either static helper class or use base IntegrationTest class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Linq3TestHelpers is a very old class that is not used in any new tests, but hasn't been refactored out yet of older classes. Once we refactor to no longer use Linq3TestHelpers it can definitely be deleted.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll try to consolidate all usages into the just introduced LinqIntegrationTest base class and remove "old" methods.


protected abstract IEnumerable<TModel> GetInitialData();

protected virtual string CollectionName => _collectionName.Value;
Copy link
Member Author

@sanych-sun sanych-sun May 28, 2024

Choose a reason for hiding this comment

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

Default collection name would be taken from the parent class name, as I suggest to have fixture as a nested class of the test class.

Copy link
Contributor

@BorisDog BorisDog left a comment

Choose a reason for hiding this comment

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

Looks like a great improvement to our tests. Waiting to see how faster our tests get when the rest of the tests are migrated.

tests/MongoDB.Driver.TestHelpers/IntegrationTest.cs Outdated Show resolved Hide resolved
}
}

public IMongoClient MongoDbClient => DriverTestConfiguration.Client;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe create own disposable MongoClient?
But then it's better to be shared across the tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Client(s) will be created per fixture and will be disposed together with fixture. See TemporaryDatabaseFixture class for implementation.

}
}

public abstract class IntegrationTest : IntegrationTest<TemporaryDatabaseFixture>
Copy link
Contributor

Choose a reason for hiding this comment

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

This one is not needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

It will be used for tests that uses TemporaryDatabaseFixture instead of TemporaryCollectionFixture. I'll show an example in next commit.

Copy link
Member Author

Choose a reason for hiding this comment

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

Please find example of usage in CSharp4066Tests test class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a better naming to reflect that. And then why not having matching class for TemporaryCollectionFixture ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've removed the class for now to avoid confusions. We might consider to re-introduce it later.


namespace MongoDB.Driver.TestHelpers
{
public abstract class TemporaryCollectionFixture<TModel> : TemporaryDatabaseFixture
Copy link
Contributor

Choose a reason for hiding this comment

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

We might need to readjust the naming, as more tests are migrated to use this approach. Something like ReadOnlyCollectionFixture...

Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

Overall I think this approach is great! Nice!


protected TFixture Fixture { get; }

protected void AssertStages(IEnumerable<BsonDocument> stages, params string[] expectedStages)
Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of these Translate methods only apply to LINQ integration tests, does it matter that they are in a more general purpose class now called IntegrationTest when not all integration tests use them?

We could derive Linq3IntegrationTest from IntegrationTest to keep the LINQ specific methods contained.

Or we could just decide it's OK for them to be bundled here. In which case the Linq3IntegrationTest class could be removed once nothing is using it any more.

protected TFixture Fixture { get; }

protected void AssertStages(IEnumerable<BsonDocument> stages, params string[] expectedStages)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Linq3TestHelpers is a very old class that is not used in any new tests, but hasn't been refactored out yet of older classes. Once we refactor to no longer use Linq3TestHelpers it can definitely be deleted.

@@ -48,7 +54,7 @@ public class CSharp702Tests : Linq3IntegrationTest
public void Query2_using_list_should_work(
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
{
var collection = GetCollection(linqProvider);
var collection = Fixture.GetCollection<Model>(linqProvider);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just:

var collection = Fixture.GetCollection(linqProvider);

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it's because there is a contradiction between TModel and BsonDocument in GetInitialData.

There is probably a better way to solve that contradiction (see GetRawInitialData proposal).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, in this particular test we are inserting the data using one model, but want to read the data as another.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've decided to make InitializeCollection virtual, so derived fixtures are free to use any initialization logic.

tests/MongoDB.Driver.TestHelpers/IntegrationTest.cs Outdated Show resolved Hide resolved
}

private class Profile
public class ProfileModel
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you remember why you renamed the class?

Copy link
Member Author

Choose a reason for hiding this comment

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

The name conflict with some namespace. I did not investigate this deep, just renamed the model to get rid of conflict.

private static readonly string __timeStamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");

protected readonly string _databaseName;
private readonly ConcurrentBag<string> _createdCollections = new();
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename field _createdCollectionNames because we are only storing the names, not the collections.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

}
}

public IMongoClient MongoDbClient => DriverTestConfiguration.Client;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would remove this property.

We want to encourage using GetClient(linqProvider) and not using this property by mistake.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
{
public class CSharp702Tests : Linq3IntegrationTest
public class CSharp702Tests : IntegrationTest<CSharp702Tests.TestDataFixture>
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe fix the class name while we're at it? Doesn't match the filename.

private class Model
{
public int Id { get; set; }
public List<string> List { get; set; }
public HashSet<string> HashSet { get; set; }
}

public class TestDataFixture : TemporaryCollectionFixture<BsonDocument>
Copy link
Contributor

Choose a reason for hiding this comment

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

public class CollectionFixture : TemporaryCollectionFixture<MyModel>

I would name this class CollectionFixture rather than TestDataFixture. To me the most important thing is that it is a "collection fixture". Initializing the test data is a secondary function.

Or Model if you don't get the same naming conflict with Amazon,SecurityServices that I was getting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done


public class TestDataFixture : TemporaryCollectionFixture<BsonDocument>
{
protected override IEnumerable<BsonDocument> GetInitialData()
Copy link
Contributor

Choose a reason for hiding this comment

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

GetRawInitialData

Copy link
Member Author

Choose a reason for hiding this comment

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

InitializeCollection is virtual now as per your another suggestion.

@sanych-sun sanych-sun requested review from rstam and BorisDog May 31, 2024 19:42
public IntegrationTestAttribute()
: base("Integration")
{

Copy link
Contributor

Choose a reason for hiding this comment

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

minor: remove empty line

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

}
}

public abstract class IntegrationTest : IntegrationTest<TemporaryDatabaseFixture>
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a better naming to reflect that. And then why not having matching class for TemporaryCollectionFixture ?

}

public IMongoClient GetClient(LinqProvider provider)
=> DriverTestConfiguration.GetLinqClient(provider);
Copy link
Contributor

Choose a reason for hiding this comment

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

The question regarding the client still remains. Do we want to use this opportunity to start working with disposable clients?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. See TemporaryDatabaseFixture for implementation.

{
database.DropCollection(collection);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not to drop the database instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently it would be almost the same, but if we approach running in parallel task - then it will be safer to remove the collection as database might be in use by other tests/fixtures.

if (string.IsNullOrEmpty(collectionName))
{
var stack = new System.Diagnostics.StackTrace();
var frame = stack.GetFrame(1); // skip 1 frame to get the calling method info
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice,
I wanted to suggest [CallerMemberName] , but we having type name is better.

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.

var frame = stack.GetFrame(1); // skip 1 frame to get the calling method info
var method = frame.GetMethod();
collectionName = $"{method.DeclaringType.Name}.{method.Name}";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It's worth having some tests that consume same collection, overwrite same data. To see how we avoid collisions.
So we are ready for parallelization.

Copy link
Member Author

Choose a reason for hiding this comment

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

For test Theories - yep. It will be more complicated, as we will need to add some extra suffixes to the names probably (based on test parameters? or test sequential number). Will try to find a way to work with this.

@@ -1,4 +1,14 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
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.

This file is really hard to review, but we need the changes to let Rider/ReSharper add Integration Tests from template. I can demo how it works on some stand up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should split templates change from the fixtures changes?
It's better to make sure that template work correctly for both VS and Rider, so we don't tie these two efforts together.

@sanych-sun sanych-sun requested a review from BorisDog June 8, 2024 00:56
@JamesKovacs JamesKovacs removed their request for review June 14, 2024 23:08
@rstam rstam removed their request for review September 30, 2024 17:22
@sanych-sun sanych-sun force-pushed the experiments/unit-tests-run-in-parallel branch 2 times, most recently from f1f0475 to c2c4ad2 Compare October 17, 2024 00:21
@sanych-sun sanych-sun requested a review from rstam October 17, 2024 16:32
@sanych-sun sanych-sun changed the title [WIP] Refactor tests to use fixture for test data population CSHARP-5338: Improve integration test performance by test fixtures Oct 17, 2024
@@ -1,4 +1,14 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should split templates change from the fixtures changes?
It's better to make sure that template work correctly for both VS and Rider, so we don't tie these two efforts together.


namespace MongoDB.Driver.Tests
{
public class DatabaseFixture: IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: space before ':'

});
}

public IMongoCollection<T> GetCollection<T>(Action<MongoClientSettings> configure = null, string collectionName = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be protected? Just to signal that these methods are intended for derived classes.


public virtual bool ResetOnEachGet => false;

public override IMongoCollection<T> GetCollection<T>(Action<MongoClientSettings> configure = null, string collectionName = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have cases where T != TDocument?


public IMongoDatabase GetDatabase(Action<MongoClientSettings> configure = null)
{
return _fixture.GetDatabase(settings =>
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: expression body ?

if (string.IsNullOrEmpty(collectionName))
{
var stack = new System.Diagnostics.StackTrace();
var frame = stack.GetFrame(2); // skip 2 frame to get the calling method info (this method and IntegrationTest method)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: frames

{
database.DropCollection(collection);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we want just to drop the DB instead?

{
private static readonly string __timeStamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");

private readonly string _databaseName = $"CsharpDriver-{__timeStamp}";
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: CSharp

private readonly HashSet<string> _usedCollections = new();

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.

Dispose the client.

{
[IntegrationTest]
public abstract class IntegrationTest<TFixture> : LoggableTestClass, IClassFixture<TFixture>
where TFixture : DatabaseFixture
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to consider reusing the client between tests as well (ICollectionFixture)?
So we don't dispose it after each tests class. For example single client for all LinqIntegrationTest?

@sanych-sun sanych-sun force-pushed the experiments/unit-tests-run-in-parallel branch from f4578b4 to 1e68de6 Compare October 29, 2024 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants