Skip to content

Commit a0d6361

Browse files
Add ExpectThrows methods for exception assertion in console tests (#103)
* Initial plan * Initial plan for ExpectThrows implementation Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> * Implement ExpectThrows and ExpectThrowsAsync methods with comprehensive tests Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> * Refactor ExpectThrows methods to eliminate nullable warnings Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> * Delete :GITHUB_ENV * Remove GitHub environment file and update .gitignore Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> * Update * pr feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> Co-authored-by: Benjamin Michaelis <benjamin@michaelis.net> Co-authored-by: Benjamin Michaelis <git@relay.benjamin.michaelis.net>
1 parent b90f675 commit a0d6361

4 files changed

Lines changed: 214 additions & 6 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,6 @@ ModelManifest.xml
214214

215215
# Jetbrains
216216
.idea
217+
218+
# GitHub Actions environment files
219+
:GITHUB_ENV

IntelliTect.TestTools.Console.Tests/ConsoleAssertTests.cs

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public void ConsoleTester_HelloWorld_DontNormalizeCRLF()
145145
{
146146
const string view = "Hello World\r\n";
147147

148-
Assert.ThrowsExactly<Exception>(() =>
148+
Assert.ThrowsExactly<ConsoleAssertException>(() =>
149149
{
150150
ConsoleAssert.Expect(view, () =>
151151
{
@@ -160,7 +160,7 @@ public void ConsoleTester_HelloWorld_DontNormalizeCRLF()
160160
[DataRow("+hello+world+")]
161161
public void ConsoleTester_OutputIncludesPluses_PlusesAreNotStripped(string consoleInput)
162162
{
163-
Exception exception = Assert.ThrowsExactly<Exception>(() =>
163+
ConsoleAssertException exception = Assert.ThrowsExactly<ConsoleAssertException>(() =>
164164
{
165165
ConsoleAssert.Expect(consoleInput, () =>
166166
{
@@ -232,4 +232,127 @@ public void ExecuteAsync_GivenVariableCRLFWithNLComparedToCRNL_Success()
232232
System.Console.WriteLine(output);
233233
});
234234
}
235-
}
235+
236+
[TestMethod]
237+
public void ExpectThrows_WhenExceptionIsThrown_CapturesException()
238+
{
239+
const string view = @"Enter a number: <<invalid
240+
>>Error: Invalid input";
241+
242+
FormatException exception = ConsoleAssert.ExpectThrows<FormatException>(view, () =>
243+
{
244+
System.Console.Write("Enter a number: ");
245+
string input = System.Console.ReadLine();
246+
System.Console.Write("Error: Invalid input");
247+
int.Parse(input); // This will throw FormatException
248+
});
249+
250+
Assert.IsNotNull(exception);
251+
Assert.IsInstanceOfType<FormatException>(exception);
252+
}
253+
254+
[TestMethod]
255+
public async Task ExpectThrowsAsync_WhenExceptionIsThrown_CapturesException()
256+
{
257+
const string view = @"Enter a number: <<invalid
258+
>>Error: Invalid input";
259+
260+
FormatException exception = await ConsoleAssert.ExpectThrowsAsync<FormatException>(view, async () =>
261+
{
262+
await Task.Yield();
263+
System.Console.Write("Enter a number: ");
264+
string input = System.Console.ReadLine();
265+
System.Console.Write("Error: Invalid input");
266+
int.Parse(input); // This will throw FormatException
267+
});
268+
269+
Assert.IsNotNull(exception);
270+
Assert.IsInstanceOfType<FormatException>(exception);
271+
}
272+
273+
[TestMethod]
274+
public void ExpectThrows_WhenNoExceptionIsThrown_ThrowsException()
275+
{
276+
const string view = @"Hello World";
277+
278+
ConsoleAssertException exception = Assert.ThrowsExactly<ConsoleAssertException>(() =>
279+
{
280+
ConsoleAssert.ExpectThrows<FormatException>(view, () =>
281+
{
282+
System.Console.Write("Hello World");
283+
// No exception thrown
284+
});
285+
});
286+
287+
StringAssert.Contains(exception.Message, "Expected exception of type FormatException was not thrown");
288+
}
289+
290+
[TestMethod]
291+
public async Task ExpectThrowsAsync_WhenNoExceptionIsThrown_ThrowsException()
292+
{
293+
const string view = @"Hello World";
294+
295+
ConsoleAssertException exception = await Assert.ThrowsExactlyAsync<ConsoleAssertException>(async () =>
296+
{
297+
await ConsoleAssert.ExpectThrowsAsync<FormatException>(view, async () =>
298+
{
299+
await Task.Yield();
300+
System.Console.Write("Hello World");
301+
// No exception thrown
302+
});
303+
});
304+
305+
StringAssert.Contains(exception.Message, "Expected exception of type FormatException was not thrown");
306+
}
307+
308+
[TestMethod]
309+
public void ExpectThrows_WithDifferentExceptionType_ThrowsOriginalException()
310+
{
311+
const string view = @"Enter a number: <<invalid
312+
>>Error: Invalid input";
313+
314+
// Expecting ArgumentException but FormatException is thrown
315+
Assert.ThrowsExactly<FormatException>(() =>
316+
{
317+
ConsoleAssert.ExpectThrows<ArgumentException>(view, () =>
318+
{
319+
System.Console.Write("Enter a number: ");
320+
string input = System.Console.ReadLine();
321+
System.Console.Write("Error: Invalid input");
322+
int.Parse(input); // This throws FormatException, not ArgumentException
323+
});
324+
});
325+
}
326+
327+
[TestMethod]
328+
public void ExpectThrows_WithNormalizeOptions_AppliesNormalization()
329+
{
330+
const string view = "Hello World\n";
331+
332+
ArgumentException exception = ConsoleAssert.ExpectThrows<ArgumentException>(view, () =>
333+
{
334+
System.Console.WriteLine("Hello World");
335+
throw new ArgumentException("Test exception");
336+
}, NormalizeOptions.NormalizeLineEndings);
337+
338+
Assert.IsNotNull(exception);
339+
StringAssert.Contains(exception.Message, "Test exception");
340+
}
341+
342+
[TestMethod]
343+
public void ExpectThrows_WithWrongExpectedOutput_ThrowsConsoleAssertException()
344+
{
345+
const string view = "Wrong output <<invalid\n>>Error: Invalid input";
346+
347+
Assert.ThrowsExactly<ConsoleAssertException>(() =>
348+
{
349+
ConsoleAssert.ExpectThrows<FormatException>(view, () =>
350+
{
351+
System.Console.Write("Enter a number: ");
352+
string input = System.Console.ReadLine();
353+
System.Console.Write("Error: Invalid input");
354+
int.Parse(input);
355+
});
356+
});
357+
}
358+
}

IntelliTect.TestTools.Console/ConsoleAssert.cs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public static void Expect<T>(string expected, Func<string[], T> func, T expected
166166

167167
if (!expectedReturn.Equals(@return))
168168
{
169-
throw new Exception($"The value returned from {nameof(func)} ({@return}) was not the {nameof(expectedReturn)}({expectedReturn}) value.");
169+
throw new ConsoleAssertException($"The value returned from {nameof(func)} ({@return}) was not the {nameof(expectedReturn)}({expectedReturn}) value.");
170170
}
171171
}
172172

@@ -468,7 +468,7 @@ private static void AssertExpectation(string expectedOutput, string output, Func
468468
bool failTest = !areEquivalentOperator(expectedOutput, output);
469469
if (failTest)
470470
{
471-
throw new Exception(GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage));
471+
throw new ConsoleAssertException(GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage));
472472
}
473473
}
474474

@@ -721,4 +721,72 @@ public static Process ExecuteProcess(string expected, string fileName, string ar
721721
AssertExpectation(expected, standardOutput, (left, right) => LikeOperator(left, right), "The values are not like (using wildcards) each other");
722722
return process;
723723
}
724-
}
724+
725+
/// <summary>
726+
/// Performs a unit test on a console-based method that is expected to throw an exception.
727+
/// A "view" of what a user would see in their console is provided as a string,
728+
/// where their input (including line-breaks) is surrounded by double
729+
/// less-than/greater-than signs, like so: "Input please: &lt;&lt;Input&gt;&gt;"
730+
/// </summary>
731+
/// <typeparam name="TException">The type of exception expected to be thrown</typeparam>
732+
/// <param name="expected">Expected "view" to be seen on the console,
733+
/// including both input and output</param>
734+
/// <param name="action">Method to be run that is expected to throw an exception</param>
735+
/// <param name="normalizeOptions">Options to normalize input and expected output</param>
736+
/// <returns>The exception that was thrown</returns>
737+
public static TException ExpectThrows<TException>(string expected,
738+
Action action,
739+
NormalizeOptions normalizeOptions = NormalizeOptions.Default)
740+
where TException : Exception
741+
{
742+
(string input, string expectedOutput) = Parse(expected);
743+
TException caughtException = null;
744+
745+
string output = Execute(input, () =>
746+
{
747+
try { action(); }
748+
catch (TException ex) { caughtException = ex; }
749+
});
750+
751+
if (caughtException is null)
752+
throw new ConsoleAssertException($"Expected exception of type {typeof(TException).Name} was not thrown.");
753+
754+
CompareOutput(output, expectedOutput, normalizeOptions, (l, r) => l == r, "Values are not equal");
755+
756+
return caughtException;
757+
}
758+
759+
/// <summary>
760+
/// Performs a unit test on a console-based async method that is expected to throw an exception.
761+
/// A "view" of what a user would see in their console is provided as a string,
762+
/// where their input (including line-breaks) is surrounded by double
763+
/// less-than/greater-than signs, like so: "Input please: &lt;&lt;Input&gt;&gt;"
764+
/// </summary>
765+
/// <typeparam name="TException">The type of exception expected to be thrown</typeparam>
766+
/// <param name="expected">Expected "view" to be seen on the console,
767+
/// including both input and output</param>
768+
/// <param name="action">Async method to be run that is expected to throw an exception</param>
769+
/// <param name="normalizeOptions">Options to normalize input and expected output</param>
770+
/// <returns>The exception that was thrown</returns>
771+
public static async Task<TException> ExpectThrowsAsync<TException>(string expected,
772+
Func<Task> action,
773+
NormalizeOptions normalizeOptions = NormalizeOptions.Default)
774+
where TException : Exception
775+
{
776+
(string input, string expectedOutput) = Parse(expected);
777+
TException caughtException = null;
778+
779+
string output = await ExecuteAsync(input, async () =>
780+
{
781+
try { await action(); }
782+
catch (TException ex) { caughtException = ex; }
783+
});
784+
785+
if (caughtException is null)
786+
throw new ConsoleAssertException($"Expected exception of type {typeof(TException).Name} was not thrown.");
787+
788+
CompareOutput(output, expectedOutput, normalizeOptions, (l, r) => l == r, "Values are not equal");
789+
790+
return caughtException;
791+
}
792+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace IntelliTect.TestTools.Console;
2+
3+
/// <summary>
4+
/// Exception thrown when a <see cref="ConsoleAssert"/> assertion fails.
5+
/// </summary>
6+
public sealed class ConsoleAssertException : Exception
7+
{
8+
/// <inheritdoc />
9+
public ConsoleAssertException(string message) : base(message) { }
10+
11+
/// <inheritdoc />
12+
public ConsoleAssertException(string message, Exception innerException)
13+
: base(message, innerException) { }
14+
}

0 commit comments

Comments
 (0)