-
-
Notifications
You must be signed in to change notification settings - Fork 106
feat: add support for reusing discovery instances in test building and property resolution #3998
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
Changes from all commits
23c8c07
cd39e30
20f9cd8
5c7bf48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Concurrent; | ||||||||||||||||||||||||||||||||||||||||||||||||
| using TUnit.Core.Interfaces; | ||||||||||||||||||||||||||||||||||||||||||||||||
| using TUnit.TestProject.Attributes; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| namespace TUnit.TestProject.Bugs._3993; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// Regression test for issue #3993: IAsyncInitializer called 4 times instead of 3 | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// when using ClassDataSource with MethodDataSource that accesses instance properties. | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||
| [EngineTest(ExpectedResult.Pass)] | ||||||||||||||||||||||||||||||||||||||||||||||||
| public class IAsyncInitializerTests | ||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| [ClassDataSource<PerTestCaseSource>(Shared = SharedType.None)] | ||||||||||||||||||||||||||||||||||||||||||||||||
| public required PerTestCaseSource PerTestCaseSource { get; init; } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| public IEnumerable<string> PerTestCaseStrings() => PerTestCaseSource.MyString; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| [Test] | ||||||||||||||||||||||||||||||||||||||||||||||||
| [MethodDataSource(nameof(PerTestCaseStrings))] | ||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task ForNTestCases_ShouldInitNTimes(string s) | ||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| await Assert.That(s).IsNotNull(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Each of the 3 test cases should have its own isolated PerTestCaseSource instance | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Since Shared = SharedType.None, each test gets a new instance | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Therefore, InitializeAsync should be called exactly 3 times (once per test) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // NOT 4 times (which would include the discovery instance) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+28
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Each of the 3 test cases should have its own isolated PerTestCaseSource instance | |
| // Since Shared = SharedType.None, each test gets a new instance | |
| // Therefore, InitializeAsync should be called exactly 3 times (once per test) | |
| // NOT 4 times (which would include the discovery instance) | |
| // There are 3 test cases, and InitializeAsync should be called exactly 3 times (once per test). | |
| // With Shared = SharedType.None, the first test case reuses the discovery instance, | |
| // while the other two get new isolated PerTestCaseSource instances. | |
| // The initialization count should be 3, not 4 (which would include an extra discovery instance). |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion has a race condition when tests run in parallel (TUnit's default behavior). The static counter _setUps is incremented during each test's initialization, but the assertion checks for an exact value of 3. The timeline could be:
- Test 1 initializes (count becomes 1), then immediately asserts count == 3 → FAIL
- Test 2 initializes (count becomes 2), then asserts count == 3 → FAIL
- Test 3 initializes (count becomes 3), then asserts count == 3 → PASS
To fix this, either:
- Add
[NotInParallel]attribute to force sequential execution - Change assertion to check a range:
.IsGreaterThanOrEqualTo(1).And.IsLessThanOrEqualTo(3) - Move the assertion to an
[After(Class)]hook that runs after all tests complete - Use a non-static counter to track per-instance initialization
| await Assert | |
| .That(PerTestCaseSource.SetUps) | |
| .IsEqualTo(3) | |
| .Because("each of the 3 test cases should be wrapped in its own isolated test class, " + | |
| "causing a call to async init, but no more than those three calls should be needed. " + | |
| "The discovery-time instance should NOT be initialized."); | |
| } | |
| [After(Class)] | |
| public static void ResetCounter() | |
| { | |
| // Assertion about SetUps moved to [After(Class)] hook to avoid race condition. | |
| } | |
| [After(Class)] | |
| public static async Task AssertSetUpsAndResetAsync() | |
| { | |
| await Assert | |
| .That(PerTestCaseSource.SetUps) | |
| .IsEqualTo(3) | |
| .Because("each of the 3 test cases should be wrapped in its own isolated test class, " + | |
| "causing a call to async init, but no more than those three calls should be needed. " + | |
| "The discovery-time instance should NOT be initialized."); |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write to static field from instance method, property, or constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "Reuse the discovery instance for the first test to avoid duplicate initialization" could be more precise. Consider clarifying that this specifically reuses the instance created for evaluating method data sources that access instance properties (via
IAccessesInstanceData), and that it only applies to the very first test in the first method data iteration with no repeats.