diff --git a/TUnit.Templates.Tests/BasicTemplateTests.cs b/TUnit.Templates.Tests/BasicTemplateTests.cs index 4f83bee5343..7ebf880ad4b 100644 --- a/TUnit.Templates.Tests/BasicTemplateTests.cs +++ b/TUnit.Templates.Tests/BasicTemplateTests.cs @@ -10,6 +10,12 @@ public async Task InstantiationTest() await Engine.Execute(Options).ConfigureAwait(false); } + [Test] + public async Task InstantiationTestWithNetFramework() + { + await Engine.Execute(OptionsWithFramework("net48")).ConfigureAwait(false); + } + [Test] public async Task InstantiationTestWithFSharp() { diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit._.verified/TUnit/HooksAndLifecycle.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit._.verified/TUnit/HooksAndLifecycle.cs index 64447d5af62..64dc762e428 100644 --- a/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit._.verified/TUnit/HooksAndLifecycle.cs +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit._.verified/TUnit/HooksAndLifecycle.cs @@ -4,7 +4,7 @@ namespace TUnit; -public class GlobalHooks +public static class GlobalHooks { [Before(TestSession)] public static Task BeforeTestSession(TestSessionContext context) diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/BasicTests.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/BasicTests.cs new file mode 100644 index 00000000000..6ad58481c5d --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/BasicTests.cs @@ -0,0 +1,53 @@ +namespace TUnit; + +public class BasicTests +{ + [Before(Class)] + public static Task BeforeClass(ClassHookContext context) + { + // Runs once before all tests in this class + return Task.CompletedTask; + } + + [After(Class)] + public static Task AfterClass(ClassHookContext context) + { + // Runs once after all tests in this class + return Task.CompletedTask; + } + + [Before(Test)] + public Task BeforeTest(TestContext context) + { + // Runs before each test in this class + return Task.CompletedTask; + } + + [After(Test)] + public Task AfterTest(TestContext context) + { + // Runs after each test in this class + return Task.CompletedTask; + } + + [Test] + public async Task Add_ReturnsSum() + { + var calculator = new Calculator(); + + var result = calculator.Add(1, 2); + + await Assert.That(result).IsEqualTo(3); + } + + [Test] + public async Task Divide_ByZero_ThrowsException() + { + var calculator = new Calculator(); + + var action = () => calculator.Divide(1, 0); + + await Assert.That(action).ThrowsException() + .WithMessage("Attempted to divide by zero."); + } +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Calculator.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Calculator.cs new file mode 100644 index 00000000000..323cfbcd8e1 --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Calculator.cs @@ -0,0 +1,10 @@ +namespace TUnit; + +public class Calculator +{ + public int Add(int a, int b) => a + b; + public int Subtract(int a, int b) => a - b; + public int Multiply(int a, int b) => a * b; + public double Divide(int a, int b) => + b == 0 ? throw new DivideByZeroException() : (double)a / b; +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Data/AdditionDataGenerator.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Data/AdditionDataGenerator.cs new file mode 100644 index 00000000000..6d375a29138 --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Data/AdditionDataGenerator.cs @@ -0,0 +1,12 @@ +namespace TUnit; + +public class AdditionDataGeneratorAttribute : DataSourceGeneratorAttribute +{ + protected override IEnumerable> GenerateDataSources( + DataGeneratorMetadata dataGeneratorMetadata) + { + yield return () => (1, 1, 2); + yield return () => (4, 5, 9); + yield return () => (-1, 1, 0); + } +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Data/InMemoryDb.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Data/InMemoryDb.cs new file mode 100644 index 00000000000..a6a674aa427 --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Data/InMemoryDb.cs @@ -0,0 +1,34 @@ +using TUnit.Core.Interfaces; + +namespace TUnit; + +public class InMemoryDb : IAsyncInitializer, IAsyncDisposable +{ + private Dictionary _store = null!; + + public Task InitializeAsync() + { + // Simulate async setup - e.g. connecting to a database or starting a container + _store = new Dictionary(); + return Task.CompletedTask; + } + + public Task SetAsync(string key, string value) + { + _store[key] = value; + return Task.CompletedTask; + } + + public Task GetAsync(string key) + { + _store.TryGetValue(key, out var value); + return Task.FromResult(value); + } + + public ValueTask DisposeAsync() + { + // Simulate async teardown - e.g. closing connections, removing containers + _store.Clear(); + return ValueTask.CompletedTask; + } +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/DataDrivenTests.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/DataDrivenTests.cs new file mode 100644 index 00000000000..1a73b35edf2 --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/DataDrivenTests.cs @@ -0,0 +1,77 @@ +namespace TUnit; + +public class DataDrivenTests +{ + [Test] + [Arguments(1, 2, 3)] + [Arguments(5, -3, 2)] + [Arguments(0, 0, 0)] + public async Task Add_WithArguments(int a, int b, int expected) + { + var calculator = new Calculator(); + + var result = calculator.Add(a, b); + + await Assert.That(result).IsEqualTo(expected); + } + + [Test] + [MethodDataSource(nameof(SubtractCases))] + public async Task Subtract_WithMethodDataSource(int a, int b, int expected) + { + var calculator = new Calculator(); + + var result = calculator.Subtract(a, b); + + await Assert.That(result).IsEqualTo(expected); + } + + [Test] + [AdditionDataGenerator] + public async Task Add_WithCustomDataGenerator(int a, int b, int expected) + { + var calculator = new Calculator(); + + var result = calculator.Add(a, b); + + await Assert.That(result).IsEqualTo(expected); + } + + [Test] + [MatrixDataSource] + public async Task Multiply_AllCombinations( + [Matrix(1, 2, 3)] int a, + [Matrix(0, 1, -1)] int b) + { + var calculator = new Calculator(); + + var result = calculator.Multiply(a, b); + + await Assert.That(result).IsEqualTo(a * b); + } + + public static IEnumerable<(int, int, int)> SubtractCases() + { + yield return (5, 3, 2); + yield return (10, 7, 3); + yield return (0, 0, 0); + } +} + +// Arguments can also be applied at the class level to parameterize the constructor +[Arguments(10)] +[Arguments(100)] +public class ClassLevelArgumentTests(int divisor) +{ + [Test] + [Arguments(100)] + [Arguments(50)] + public async Task Divide_WithClassAndMethodArguments(int dividend) + { + var calculator = new Calculator(); + + var result = calculator.Divide(dividend, divisor); + + await Assert.That(result).IsGreaterThan(0); + } +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/DependencyInjectionTests.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/DependencyInjectionTests.cs new file mode 100644 index 00000000000..ff40313c0ec --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/DependencyInjectionTests.cs @@ -0,0 +1,47 @@ +namespace TUnit; + +public class DependencyInjectionTests +{ + // A new InMemoryDb is created for each test + [Test] + [ClassDataSource] + public async Task Database_SetAndGet(InMemoryDb db) + { + await db.SetAsync("key", "value"); + + var result = await db.GetAsync("key"); + + await Assert.That(result).IsEqualTo("value"); + } + + // The same InMemoryDb instance is shared across all tests in this class + [Test] + [ClassDataSource(Shared = SharedType.PerClass)] + public async Task Database_SharedPerClass(InMemoryDb db) + { + await db.SetAsync("shared", "data"); + + var result = await db.GetAsync("shared"); + + await Assert.That(result).IsNotNull(); + } +} + +// ClassDataSource can also be applied at the class level +[ClassDataSource(Shared = SharedType.PerTestSession)] +public class SharedDatabaseTests(InMemoryDb db) +{ + // Or injected as a property instead of a constructor parameter + [ClassDataSource] + public required Calculator Calculator { get; init; } + + [Test] + public async Task Calculator_ResultCanBeStored() + { + var result = Calculator.Add(2, 3); + + await db.SetAsync("result", result.ToString()); + + await Assert.That(await db.GetAsync("result")).IsEqualTo("5"); + } +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/HooksAndLifecycle.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/HooksAndLifecycle.cs new file mode 100644 index 00000000000..c3eefa0ec66 --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/HooksAndLifecycle.cs @@ -0,0 +1,22 @@ + +[assembly: TUnit.Polyfills.ExcludeFromCodeCoverage] + +namespace TUnit; + +public static class GlobalHooks +{ + [Before(TestSession)] + public static Task BeforeTestSession(TestSessionContext context) + { + // Runs once before all tests - e.g. start a test container, seed a database + return Task.CompletedTask; + } + + [After(TestSession)] + public static Task AfterTestSession(TestSessionContext context) + { + // Runs once after all tests - e.g. stop containers, clean up resources + return Task.CompletedTask; + } + +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Polyfills/ExcludeFromCodeCoverageAttribute.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Polyfills/ExcludeFromCodeCoverageAttribute.cs new file mode 100644 index 00000000000..1ecef0281bd --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/Polyfills/ExcludeFromCodeCoverageAttribute.cs @@ -0,0 +1,7 @@ +namespace TUnit.Polyfills; + +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event, Inherited = false, AllowMultiple = false)] +internal sealed class ExcludeFromCodeCoverageAttribute : Attribute +{ + public string? Justification { get; set; } +} diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/TUnit.csproj b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/TUnit.csproj new file mode 100644 index 00000000000..a6faca00d2f --- /dev/null +++ b/TUnit.Templates.Tests/Snapshots/InstantiationTestWithNetFramework.TUnit.--framework#net48.verified/TUnit/TUnit.csproj @@ -0,0 +1,17 @@ + + + + enable + enable + Exe + net48 + preview + false + + + + + + + + diff --git a/TUnit.Templates.Tests/TemplateTestBase.cs b/TUnit.Templates.Tests/TemplateTestBase.cs index b55be5708b0..fbfd1ab1d17 100644 --- a/TUnit.Templates.Tests/TemplateTestBase.cs +++ b/TUnit.Templates.Tests/TemplateTestBase.cs @@ -34,6 +34,18 @@ public abstract partial class TemplateTestBase : IDisposable .AddScrubber(ScrubVersions, "vbproj") ); + protected TemplateVerifierOptions OptionsWithFramework(string framework) => + new TemplateVerifierOptions(TemplateShortName) + { + TemplatePath = Path.Combine(TestContext.OutputDirectory!, "content", TemplateShortName), + TemplateSpecificArgs = ["--framework", framework], + }.WithCustomScrubbers( + ScrubbersDefinition.Empty + .AddScrubber(ScrubVersions, "csproj") + .AddScrubber(ScrubVersions, "fsproj") + .AddScrubber(ScrubVersions, "vbproj") + ); + private const string ScrubbedVersion = "0.0.0-scrubbed"; private static void ScrubVersions(StringBuilder sb) diff --git a/TUnit.Templates/content/TUnit/.template.config/template.json b/TUnit.Templates/content/TUnit/.template.config/template.json index a2c1976f9a8..3215c63b8d8 100644 --- a/TUnit.Templates/content/TUnit/.template.config/template.json +++ b/TUnit.Templates/content/TUnit/.template.config/template.json @@ -15,6 +15,40 @@ "type": "project" }, "sourceName": "TestProject", - "preferNameDirectory": true + "preferNameDirectory": true, + "symbols": { + "framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { "choice": "net10.0", "description": "Target .NET 10.0" }, + { "choice": "net9.0", "description": "Target .NET 9.0" }, + { "choice": "net8.0", "description": "Target .NET 8.0" }, + { "choice": "net481", "description": "Target .NET Framework 4.8.1" }, + { "choice": "net48", "description": "Target .NET Framework 4.8" }, + { "choice": "net472", "description": "Target .NET Framework 4.7.2" }, + { "choice": "net462", "description": "Target .NET Framework 4.6.2" } + ], + "defaultValue": "net10.0", + "replaces": "net10.0" + }, + "IsNetFramework": { + "type": "computed", + "value": "(framework == \"net462\" || framework == \"net472\" || framework == \"net48\" || framework == \"net481\")" + } + }, + "sources": [ + { + "modifiers": [ + { + "condition": "(!IsNetFramework)", + "exclude": [ + "Polyfills/**" + ] + } + ] + } + ] } \ No newline at end of file diff --git a/TUnit.Templates/content/TUnit/HooksAndLifecycle.cs b/TUnit.Templates/content/TUnit/HooksAndLifecycle.cs index a47c4182ad9..c20e761dcbb 100644 --- a/TUnit.Templates/content/TUnit/HooksAndLifecycle.cs +++ b/TUnit.Templates/content/TUnit/HooksAndLifecycle.cs @@ -1,10 +1,16 @@ +#if (!IsNetFramework) using System.Diagnostics.CodeAnalysis; +#endif +#if (IsNetFramework) +[assembly: TestProject.Polyfills.ExcludeFromCodeCoverage] +#else [assembly: ExcludeFromCodeCoverage] +#endif namespace TestProject; -public class GlobalHooks +public static class GlobalHooks { [Before(TestSession)] public static Task BeforeTestSession(TestSessionContext context) diff --git a/TUnit.Templates/content/TUnit/Polyfills/ExcludeFromCodeCoverageAttribute.cs b/TUnit.Templates/content/TUnit/Polyfills/ExcludeFromCodeCoverageAttribute.cs new file mode 100644 index 00000000000..89b039fb63b --- /dev/null +++ b/TUnit.Templates/content/TUnit/Polyfills/ExcludeFromCodeCoverageAttribute.cs @@ -0,0 +1,7 @@ +namespace TestProject.Polyfills; + +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event, Inherited = false, AllowMultiple = false)] +internal sealed class ExcludeFromCodeCoverageAttribute : Attribute +{ + public string? Justification { get; set; } +} diff --git a/TUnit.Templates/content/TUnit/TestProject.csproj b/TUnit.Templates/content/TUnit/TestProject.csproj index c564cd0e5a6..8f279359f7e 100644 --- a/TUnit.Templates/content/TUnit/TestProject.csproj +++ b/TUnit.Templates/content/TUnit/TestProject.csproj @@ -5,10 +5,17 @@ enable Exe net10.0 + + preview + false + + + +