Skip to content

Commit 8081f29

Browse files
PandaMagnusKeboo
andauthored
Finish wiring up multiple return values using "BlockData" objects (#77)
* Finish wiring up multiple return values using "BlockData" objects * Update README with test data and BlockData details Co-authored-by: Kevin B <[email protected]> * Finish requested changes to BlockData --------- Co-authored-by: Kevin B <[email protected]>
1 parent e2a0755 commit 8081f29

File tree

7 files changed

+254
-18
lines changed

7 files changed

+254
-18
lines changed

IntelliTect.TestTools.TestFramework.Tests/TestCaseTests/MultipleDependencyTests.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using IntelliTect.TestTools.TestFramework.Tests.TestData.Dependencies;
2-
using IntelliTect.TestTools.TestFramework.Tests.TestData.TestBlocks;
1+
using IntelliTect.TestTools.TestFramework.Tests.TestData.TestBlocks;
2+
using System;
3+
using System.Threading.Tasks;
34
using Xunit;
45

56
namespace IntelliTect.TestTools.TestFramework.Tests.TestCaseTests
67
{
7-
public class MultipleDependencyTests
8+
public class MultipleDependencyTests : TestBase
89
{
910
[Fact]
1011
public void ReturnDuplicateTypesDoesNotThrow()
@@ -38,5 +39,41 @@ public void FetchByObjectInstanceForMultipleDependencies()
3839
// Assert
3940
Assert.True(tc.Passed);
4041
}
42+
43+
[Fact]
44+
public async Task ReturnMultipleObjectsStoresEachObjectSeparately()
45+
{
46+
// Arrange
47+
TestCase tc = new TestBuilder()
48+
.AddTestBlock<ExampleBlockWithMultipleReturns>(true)
49+
.AddTestBlock<ExampleTestBlockWithExecuteArg>()
50+
.AddTestBlock<ExampleTestBlockWithBoolReturn>()
51+
.Build();
52+
53+
// Act
54+
await tc.ExecuteAsync();
55+
56+
// Assert
57+
Assert.True(tc.Passed);
58+
}
59+
60+
[Fact]
61+
public async Task ReturnMultipleObjectsExecutesSubsequentTestBlocks()
62+
{
63+
// Arrange
64+
65+
TestCase tc = new TestBuilder()
66+
.AddTestBlock<ExampleBlockWithMultipleReturns>(false)
67+
.AddTestBlock<ExampleTestBlockWithExecuteArg>()
68+
.AddTestBlock<ExampleTestBlockWithBoolReturn>()
69+
.Build();
70+
71+
// Act
72+
var result = await Assert.ThrowsAsync<TestCaseException>(tc.ExecuteAsync);
73+
74+
// Assert
75+
Assert.False(tc.Passed);
76+
Assert.IsType<DivideByZeroException>(result.InnerException);
77+
}
4178
}
4279
}

IntelliTect.TestTools.TestFramework.Tests/TestData/TestBlocks/MultipleDependencyBlocks.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,12 @@ public void Execute(string inputText, int inputNumber)
2121
Assert.Equal(1234, inputNumber);
2222
}
2323
}
24+
25+
public class ExampleBlockWithMultipleReturns : TestBlock
26+
{
27+
public BlockData<string, bool> Execute(bool returnValue)
28+
{
29+
return new BlockData<string, bool>("Testing", returnValue);
30+
}
31+
}
2432
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using Xunit;
3+
4+
namespace IntelliTect.TestTools.TestFramework.Tests;
5+
6+
public class TestDataTests
7+
{
8+
[Fact]
9+
public void BlockDataT1T2ThrowsExceptionOnDuplicateTypes()
10+
{
11+
var exception = Assert.Throws<TypeInitializationException>(() =>
12+
{
13+
var _ = new BlockData<int, int>(1, 2);
14+
});
15+
Assert.IsType<InvalidOperationException>(exception.InnerException);
16+
Assert.Equal("Duplicate type found: Int32 appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container.",
17+
exception.InnerException.Message);
18+
}
19+
20+
[Fact]
21+
public void BlockDataT1T2T3ThrowsExceptionOnDuplicateTypes()
22+
{
23+
var exception = Assert.Throws<TypeInitializationException>(() =>
24+
{
25+
var _ = new BlockData<int, bool, int>(1, true, 2);
26+
});
27+
var invalidOp = Assert.IsType<InvalidOperationException>(exception.InnerException);
28+
Assert.Equal("Duplicate type found: Int32 appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container.",
29+
invalidOp.Message);
30+
}
31+
32+
[Fact]
33+
public void BlockDataT1T2T3T4ThrowsExceptionOnDuplicateTypes()
34+
{
35+
var exception = Assert.Throws<TypeInitializationException>(() =>
36+
{
37+
var _ = new BlockData<bool, double, int, int>(false, 0.5, 1, 2);
38+
});
39+
Assert.NotNull(exception.InnerException);
40+
Assert.Equal(typeof(InvalidOperationException), exception.InnerException.GetType());
41+
Assert.Equal("Duplicate type found: Int32 appears multiple times. BlockData must use different types to avoid unexpected behavior by the TestCase DI Container.",
42+
exception.InnerException.Message);
43+
}
44+
45+
[Fact]
46+
public void BlockDataT1T2DoesNotThrowExceptionWithUniqueTypes()
47+
{
48+
BlockData<int, bool> blockData = new(1, true);
49+
Assert.Equal(1, blockData.Data1);
50+
Assert.True(blockData.Data2);
51+
}
52+
53+
[Fact]
54+
public void BlockDataT1T2T3DoesNotThrowExceptionWithUniqueTypes()
55+
{
56+
BlockData<ArgumentException, Exception, bool> blockData = new(new ArgumentException(), new Exception(), true);
57+
Assert.IsType<ArgumentException>(blockData.Data1);
58+
Assert.IsType<Exception>(blockData.Data2);
59+
Assert.True(blockData.Data3);
60+
}
61+
62+
[Fact]
63+
public void BlockDataT1T2T3T4DoesNotThrowExceptionWithUniqueTypes()
64+
{
65+
BlockData<int, bool, string, double> blockData = new(1, true, "", 0.5);
66+
Assert.Equal(1, blockData.Data1);
67+
Assert.True(blockData.Data2);
68+
Assert.Equal("", blockData.Data3);
69+
Assert.Equal(0.5, blockData.Data4);
70+
}
71+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace IntelliTect.TestTools.TestFramework;
5+
6+
public interface IBlockData
7+
{
8+
public List<KeyValuePair<Type, object?>> Data { get; }
9+
}
10+
11+
public class BlockData<T1, T2>(T1 data1, T2 data2) : IBlockData
12+
{
13+
static BlockData()
14+
{
15+
ValidateData.ValidateUniqueTypes(typeof(T1), typeof(T2));
16+
}
17+
18+
public T1 Data1 => data1;
19+
public T2 Data2 => data2;
20+
21+
22+
List<KeyValuePair<Type, object?>> IBlockData.Data { get; } =
23+
[
24+
new KeyValuePair<Type, object?>(typeof(T1), data1),
25+
new KeyValuePair<Type, object?>(typeof(T2), data2)
26+
];
27+
}
28+
29+
public class BlockData<T1, T2, T3>(T1 data1, T2 data2, T3 data3) : IBlockData
30+
{
31+
static BlockData()
32+
{
33+
ValidateData.ValidateUniqueTypes(typeof(T1), typeof(T2), typeof(T3));
34+
}
35+
36+
public T1 Data1 => data1;
37+
public T2 Data2 => data2;
38+
public T3 Data3 => data3;
39+
40+
List<KeyValuePair<Type, object?>> IBlockData.Data { get; } =
41+
[
42+
new KeyValuePair<Type, object?>(typeof(T1), data1),
43+
new KeyValuePair<Type, object?>(typeof(T2), data2),
44+
new KeyValuePair<Type, object?>(typeof(T3), data3)
45+
];
46+
}
47+
48+
public class BlockData<T1, T2, T3, T4>(T1 data1, T2 data2, T3 data3, T4 data4) : IBlockData
49+
{
50+
static BlockData()
51+
{
52+
ValidateData.ValidateUniqueTypes(typeof(T1), typeof(T2), typeof(T3), typeof(T4));
53+
}
54+
55+
public T1 Data1 => data1;
56+
public T2 Data2 => data2;
57+
public T3 Data3 => data3;
58+
public T4 Data4 => data4;
59+
60+
List<KeyValuePair<Type, object?>> IBlockData.Data { get; } =
61+
[
62+
new KeyValuePair<Type, object?>(typeof(T1), data1),
63+
new KeyValuePair<Type, object?>(typeof(T2), data2),
64+
new KeyValuePair<Type, object?>(typeof(T3), data3),
65+
new KeyValuePair<Type, object?>(typeof(T4), data4)
66+
];
67+
}
68+
69+
internal static class ValidateData
70+
{
71+
internal static void ValidateUniqueTypes(params Type[] types)
72+
{
73+
HashSet<Type> seenTypes = [];
74+
foreach(Type type in types)
75+
{
76+
if (!seenTypes.Add(type))
77+
{
78+
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.");
79+
}
80+
}
81+
}
82+
}

IntelliTect.TestTools.TestFramework/TestBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,18 @@ private void GatherDependencies(
354354

355355
if (executeReturns != typeof(void))
356356
{
357-
outputs.Add(executeReturns);
357+
if (executeReturns.GetInterfaces().Any(x => x == typeof(IBlockData)))
358+
{
359+
Type[] blockData = executeReturns.GenericTypeArguments;
360+
foreach (Type bd in blockData)
361+
{
362+
outputs.Add(bd);
363+
}
364+
}
365+
else
366+
{
367+
outputs.Add(executeReturns);
368+
}
358369
}
359370
}
360371

IntelliTect.TestTools.TestFramework/TestCase.cs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,15 @@ public async Task ExecuteAsync()
142142
{
143143
throw new TestCaseException("Test case failed.", TestBlockException);
144144
}
145-
else if(TestBlockException is not null
145+
else if (TestBlockException is not null
146146
&& ThrowOnFinallyBlockException
147147
&& FinallyBlockExceptions.Any())
148148
{
149149
FinallyBlockExceptions.Insert(0, TestBlockException);
150150
throw new AggregateException("Test case failed and finally blocks failed.",
151151
FinallyBlockExceptions);
152152
}
153-
else if(TestBlockException is null
153+
else if (TestBlockException is null
154154
&& ThrowOnFinallyBlockException
155155
&& FinallyBlockExceptions.Any())
156156
{
@@ -199,7 +199,7 @@ private bool TryGetBlock(IServiceScope scope, Block block, out object blockInsta
199199
_ = TryBuildBlock(scope, block, out foundBlock);
200200
}
201201

202-
if(foundBlock is not null)
202+
if (foundBlock is not null)
203203
{
204204
blockInstance = foundBlock;
205205
result = true;
@@ -233,7 +233,7 @@ private bool TryBuildBlock(IServiceScope scope, Block block, out object? blockIn
233233
object? obj = ActivateObject(scope, block, c.ParameterType, "constructor argument");
234234
if (obj is null)
235235
{
236-
if(!CheckForITestLogger(c.ParameterType))
236+
if (!CheckForITestLogger(c.ParameterType))
237237
{
238238
blockInstance = null;
239239
return false;
@@ -267,7 +267,7 @@ private bool TrySetBlockProperties(IServiceScope scope, Block block, object bloc
267267
object? obj = ActivateObject(scope, block, prop.PropertyType, "property");
268268
if (obj is null)
269269
{
270-
if(CheckForITestLogger(prop.PropertyType))
270+
if (CheckForITestLogger(prop.PropertyType))
271271
{
272272
continue;
273273
}
@@ -288,12 +288,12 @@ private bool TryGetExecuteArguments(IServiceScope scope, Block block, out List<o
288288
foreach (ParameterInfo? ep in block.ExecuteParams)
289289
{
290290
object? obj = null;
291-
if(block.ExecuteArgumentOverrides.Count > 0)
291+
if (block.ExecuteArgumentOverrides.Count > 0)
292292
{
293293
block.ExecuteArgumentOverrides.TryGetValue(ep.ParameterType, out obj);
294294
}
295295

296-
if(obj is null)
296+
if (obj is null)
297297
{
298298
obj = ActivateObject(scope, block, ep.ParameterType, "execute method argument");
299299
if (obj is null)
@@ -325,9 +325,9 @@ private bool TryGetExecuteArguments(IServiceScope scope, Block block, out List<o
325325
// Is the below check worth it?
326326
// It is avoided if the test block asks for an interface if the dependency is implementing an interface.
327327
// HOWEVER, this would facilitate injecting multiple different implementations in a test.
328-
if(obj is null)
328+
if (obj is null)
329329
{
330-
foreach(var i in objectType.GetInterfaces())
330+
foreach (var i in objectType.GetInterfaces())
331331
{
332332
IEnumerable<object?> objs = scope.ServiceProvider.GetServices(i);
333333
obj = objs.FirstOrDefault(o => o?.GetType() == objectType);
@@ -380,7 +380,7 @@ private async Task<bool> TryRunBlock(Block block, object blockInstance, List<obj
380380
{
381381
dynamic asyncOutput = block.ExecuteMethod.Invoke(blockInstance, executeArgs.ToArray());
382382
await asyncOutput;
383-
if(block.AsyncReturnType != typeof(void))
383+
if (block.AsyncReturnType != typeof(void))
384384
{
385385
output = asyncOutput.GetAwaiter().GetResult();
386386
}
@@ -389,12 +389,28 @@ private async Task<bool> TryRunBlock(Block block, object blockInstance, List<obj
389389
{
390390
output = block.ExecuteMethod.Invoke(blockInstance, executeArgs.ToArray());
391391
}
392-
392+
393393
if (output is not null)
394394
{
395-
Log?.TestBlockOutput(output);
396-
BlockOutput.Remove(output.GetType());
397-
BlockOutput.Add(output.GetType(), output);
395+
if (output is IBlockData blockData)
396+
{
397+
foreach (KeyValuePair<Type, object?> dataPoint in blockData.Data)
398+
{
399+
if (dataPoint.Value is not null)
400+
{
401+
Log?.TestBlockOutput(dataPoint.Value);
402+
BlockOutput.Remove(dataPoint.Key);
403+
BlockOutput.Add(dataPoint.Key, dataPoint.Value);
404+
}
405+
}
406+
}
407+
else
408+
{
409+
Log?.TestBlockOutput(output);
410+
Type outputType = output.GetType();
411+
BlockOutput.Remove(outputType);
412+
BlockOutput.Add(outputType, output);
413+
}
398414
}
399415
result = true;
400416
}

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,15 @@ public async Task Test1()
120120

121121
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<bool>, TestFramework will capture the bool result to use.
122122

123+
Test Data
124+
-----
125+
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:
126+
```
127+
public BlockData<string, bool> Execute()
128+
{
129+
return new BlockData<string, bool>("Testing", true);
130+
}
131+
```
132+
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.
133+
123134
More in depth examples are coming later!

0 commit comments

Comments
 (0)