diff --git a/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/MultipleDependencyTests.cs b/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/MultipleDependencyTests.cs index 56822df..2dd0a33 100644 --- a/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/MultipleDependencyTests.cs +++ b/IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/MultipleDependencyTests.cs @@ -1,10 +1,11 @@ -using IntelliTect.TestTools.TestFramework.Tests.TestData.Dependencies; -using IntelliTect.TestTools.TestFramework.Tests.TestData.TestBlocks; +using IntelliTect.TestTools.TestFramework.Tests.TestData.TestBlocks; +using System; +using System.Threading.Tasks; using Xunit; namespace IntelliTect.TestTools.TestFramework.Tests.TestCaseTests { - public class MultipleDependencyTests + public class MultipleDependencyTests : TestBase { [Fact] public void ReturnDuplicateTypesDoesNotThrow() @@ -38,5 +39,41 @@ public void FetchByObjectInstanceForMultipleDependencies() // Assert Assert.True(tc.Passed); } + + [Fact] + public async Task ReturnMultipleObjectsStoresEachObjectSeparately() + { + // Arrange + TestCase tc = new TestBuilder() + .AddTestBlock(true) + .AddTestBlock() + .AddTestBlock() + .Build(); + + // Act + await tc.ExecuteAsync(); + + // Assert + Assert.True(tc.Passed); + } + + [Fact] + public async Task ReturnMultipleObjectsExecutesSubsequentTestBlocks() + { + // Arrange + + TestCase tc = new TestBuilder() + .AddTestBlock(false) + .AddTestBlock() + .AddTestBlock() + .Build(); + + // Act + var result = await Assert.ThrowsAsync(tc.ExecuteAsync); + + // Assert + Assert.False(tc.Passed); + Assert.IsType(result.InnerException); + } } } diff --git a/IntelliTect.TestTools.TestFramework.Tests/TestData/TestBlocks/MultipleDependencyBlocks.cs b/IntelliTect.TestTools.TestFramework.Tests/TestData/TestBlocks/MultipleDependencyBlocks.cs index 21dc1ba..6f362a4 100644 --- a/IntelliTect.TestTools.TestFramework.Tests/TestData/TestBlocks/MultipleDependencyBlocks.cs +++ b/IntelliTect.TestTools.TestFramework.Tests/TestData/TestBlocks/MultipleDependencyBlocks.cs @@ -21,4 +21,12 @@ public void Execute(string inputText, int inputNumber) Assert.Equal(1234, inputNumber); } } + + public class ExampleBlockWithMultipleReturns : TestBlock + { + public BlockData Execute(bool returnValue) + { + return new BlockData("Testing", returnValue); + } + } } diff --git a/IntelliTect.TestTools.TestFramework.Tests/TestDataTests.cs b/IntelliTect.TestTools.TestFramework.Tests/TestDataTests.cs new file mode 100644 index 0000000..cd472ba --- /dev/null +++ b/IntelliTect.TestTools.TestFramework.Tests/TestDataTests.cs @@ -0,0 +1,71 @@ +using System; +using Xunit; + +namespace IntelliTect.TestTools.TestFramework.Tests; + +public class TestDataTests +{ + [Fact] + public void BlockDataT1T2ThrowsExceptionOnDuplicateTypes() + { + var exception = Assert.Throws(() => + { + var _ = new BlockData(1, 2); + }); + Assert.IsType(exception.InnerException); + Assert.Equal("Duplicate type found: Int32 appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container.", + exception.InnerException.Message); + } + + [Fact] + public void BlockDataT1T2T3ThrowsExceptionOnDuplicateTypes() + { + var exception = Assert.Throws(() => + { + var _ = new BlockData(1, true, 2); + }); + var invalidOp = Assert.IsType(exception.InnerException); + Assert.Equal("Duplicate type found: Int32 appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container.", + invalidOp.Message); + } + + [Fact] + public void BlockDataT1T2T3T4ThrowsExceptionOnDuplicateTypes() + { + var exception = Assert.Throws(() => + { + var _ = new BlockData(false, 0.5, 1, 2); + }); + Assert.NotNull(exception.InnerException); + Assert.Equal(typeof(InvalidOperationException), exception.InnerException.GetType()); + Assert.Equal("Duplicate type found: Int32 appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container.", + exception.InnerException.Message); + } + + [Fact] + public void BlockDataT1T2DoesNotThrowExceptionWithUniqueTypes() + { + BlockData blockData = new(1, true); + Assert.Equal(1, blockData.Data1); + Assert.True(blockData.Data2); + } + + [Fact] + public void BlockDataT1T2T3DoesNotThrowExceptionWithUniqueTypes() + { + BlockData blockData = new(new ArgumentException(), new Exception(), true); + Assert.IsType(blockData.Data1); + Assert.IsType(blockData.Data2); + Assert.True(blockData.Data3); + } + + [Fact] + public void BlockDataT1T2T3T4DoesNotThrowExceptionWithUniqueTypes() + { + BlockData blockData = new(1, true, "", 0.5); + Assert.Equal(1, blockData.Data1); + Assert.True(blockData.Data2); + Assert.Equal("", blockData.Data3); + Assert.Equal(0.5, blockData.Data4); + } +} diff --git a/IntelliTect.TestTools.TestFramework/BlockData.cs b/IntelliTect.TestTools.TestFramework/BlockData.cs new file mode 100644 index 0000000..005b854 --- /dev/null +++ b/IntelliTect.TestTools.TestFramework/BlockData.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace IntelliTect.TestTools.TestFramework; + +public interface IBlockData +{ + public List> Data { get; } +} + +public class BlockData(T1 data1, T2 data2) : IBlockData +{ + static BlockData() + { + ValidateData.ValidateUniqueTypes(typeof(T1), typeof(T2)); + } + + public T1 Data1 => data1; + public T2 Data2 => data2; + + + List> IBlockData.Data { get; } = + [ + new KeyValuePair(typeof(T1), data1), + new KeyValuePair(typeof(T2), data2) + ]; +} + +public class BlockData(T1 data1, T2 data2, T3 data3) : IBlockData +{ + static BlockData() + { + ValidateData.ValidateUniqueTypes(typeof(T1), typeof(T2), typeof(T3)); + } + + public T1 Data1 => data1; + public T2 Data2 => data2; + public T3 Data3 => data3; + + List> IBlockData.Data { get; } = + [ + new KeyValuePair(typeof(T1), data1), + new KeyValuePair(typeof(T2), data2), + new KeyValuePair(typeof(T3), data3) + ]; +} + +public class BlockData(T1 data1, T2 data2, T3 data3, T4 data4) : IBlockData +{ + static BlockData() + { + ValidateData.ValidateUniqueTypes(typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public T1 Data1 => data1; + public T2 Data2 => data2; + public T3 Data3 => data3; + public T4 Data4 => data4; + + List> IBlockData.Data { get; } = + [ + new KeyValuePair(typeof(T1), data1), + new KeyValuePair(typeof(T2), data2), + new KeyValuePair(typeof(T3), data3), + new KeyValuePair(typeof(T4), data4) + ]; +} + +internal static class ValidateData +{ + internal static void ValidateUniqueTypes(params Type[] types) + { + HashSet seenTypes = []; + foreach(Type type in types) + { + if (!seenTypes.Add(type)) + { + throw new InvalidOperationException($"Duplicate type found: {type.Name} appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container."); + } + } + } +} \ No newline at end of file diff --git a/IntelliTect.TestTools.TestFramework/TestBuilder.cs b/IntelliTect.TestTools.TestFramework/TestBuilder.cs index 9cba61b..efc1515 100644 --- a/IntelliTect.TestTools.TestFramework/TestBuilder.cs +++ b/IntelliTect.TestTools.TestFramework/TestBuilder.cs @@ -354,7 +354,18 @@ private void GatherDependencies( if (executeReturns != typeof(void)) { - outputs.Add(executeReturns); + if (executeReturns.GetInterfaces().Any(x => x == typeof(IBlockData))) + { + Type[] blockData = executeReturns.GenericTypeArguments; + foreach (Type bd in blockData) + { + outputs.Add(bd); + } + } + else + { + outputs.Add(executeReturns); + } } } diff --git a/IntelliTect.TestTools.TestFramework/TestCase.cs b/IntelliTect.TestTools.TestFramework/TestCase.cs index 7b42dc8..4efdffc 100644 --- a/IntelliTect.TestTools.TestFramework/TestCase.cs +++ b/IntelliTect.TestTools.TestFramework/TestCase.cs @@ -142,7 +142,7 @@ public async Task ExecuteAsync() { throw new TestCaseException("Test case failed.", TestBlockException); } - else if(TestBlockException is not null + else if (TestBlockException is not null && ThrowOnFinallyBlockException && FinallyBlockExceptions.Any()) { @@ -150,7 +150,7 @@ public async Task ExecuteAsync() throw new AggregateException("Test case failed and finally blocks failed.", FinallyBlockExceptions); } - else if(TestBlockException is null + else if (TestBlockException is null && ThrowOnFinallyBlockException && FinallyBlockExceptions.Any()) { @@ -199,7 +199,7 @@ private bool TryGetBlock(IServiceScope scope, Block block, out object blockInsta _ = TryBuildBlock(scope, block, out foundBlock); } - if(foundBlock is not null) + if (foundBlock is not null) { blockInstance = foundBlock; result = true; @@ -233,7 +233,7 @@ private bool TryBuildBlock(IServiceScope scope, Block block, out object? blockIn object? obj = ActivateObject(scope, block, c.ParameterType, "constructor argument"); if (obj is null) { - if(!CheckForITestLogger(c.ParameterType)) + if (!CheckForITestLogger(c.ParameterType)) { blockInstance = null; return false; @@ -267,7 +267,7 @@ private bool TrySetBlockProperties(IServiceScope scope, Block block, object bloc object? obj = ActivateObject(scope, block, prop.PropertyType, "property"); if (obj is null) { - if(CheckForITestLogger(prop.PropertyType)) + if (CheckForITestLogger(prop.PropertyType)) { continue; } @@ -288,12 +288,12 @@ private bool TryGetExecuteArguments(IServiceScope scope, Block block, out List 0) + if (block.ExecuteArgumentOverrides.Count > 0) { block.ExecuteArgumentOverrides.TryGetValue(ep.ParameterType, out obj); } - if(obj is null) + if (obj is null) { obj = ActivateObject(scope, block, ep.ParameterType, "execute method argument"); if (obj is null) @@ -325,9 +325,9 @@ private bool TryGetExecuteArguments(IServiceScope scope, Block block, out List objs = scope.ServiceProvider.GetServices(i); obj = objs.FirstOrDefault(o => o?.GetType() == objectType); @@ -380,7 +380,7 @@ private async Task TryRunBlock(Block block, object blockInstance, List TryRunBlock(Block block, object blockInstance, List dataPoint in blockData.Data) + { + if (dataPoint.Value is not null) + { + Log?.TestBlockOutput(dataPoint.Value); + BlockOutput.Remove(dataPoint.Key); + BlockOutput.Add(dataPoint.Key, dataPoint.Value); + } + } + } + else + { + Log?.TestBlockOutput(output); + Type outputType = output.GetType(); + BlockOutput.Remove(outputType); + BlockOutput.Add(outputType, output); + } } result = true; } diff --git a/README.md b/README.md index c3327d7..91d3fe2 100644 --- a/README.md +++ b/README.md @@ -120,4 +120,15 @@ public async Task Test1() Also note that current behavior is that TestFramework will take the result of the awaited test block task and use that for future test block dependencies. If you have a test block that returns Task, TestFramework will capture the bool result to use. +Test Data +----- +In normal situations, test blocks typically only return a single datapoint if any data is returned at all. The underlying container picks that object up and uses it for subsequent test blocks like demonstrated in the example project or unit tests. In some cases, it's not feasible to return just a single object, and so the BlockData object can handle returning 2 - 4 different data points. The execute method would look similar to this: +``` +public BlockData Execute() +{ + return new BlockData("Testing", true); +} +``` +Note that in the out of the box BlockData objects, it will validate that the types are all unique; otherwise duplicate types would simply overwrite each other and cause unexpected behavior in subsquent block executions. + More in depth examples are coming later!