diff --git a/.gitignore b/.gitignore index 46f0db5..584d96c 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,6 @@ ModelManifest.xml # Jetbrains .idea + +# GitHub Actions environment files +:GITHUB_ENV diff --git a/IntelliTect.TestTools.Console.Tests/ConsoleAssertTests.cs b/IntelliTect.TestTools.Console.Tests/ConsoleAssertTests.cs index e6c76b5..033625b 100644 --- a/IntelliTect.TestTools.Console.Tests/ConsoleAssertTests.cs +++ b/IntelliTect.TestTools.Console.Tests/ConsoleAssertTests.cs @@ -145,7 +145,7 @@ public void ConsoleTester_HelloWorld_DontNormalizeCRLF() { const string view = "Hello World\r\n"; - Assert.ThrowsExactly(() => + Assert.ThrowsExactly(() => { ConsoleAssert.Expect(view, () => { @@ -160,7 +160,7 @@ public void ConsoleTester_HelloWorld_DontNormalizeCRLF() [DataRow("+hello+world+")] public void ConsoleTester_OutputIncludesPluses_PlusesAreNotStripped(string consoleInput) { - Exception exception = Assert.ThrowsExactly(() => + ConsoleAssertException exception = Assert.ThrowsExactly(() => { ConsoleAssert.Expect(consoleInput, () => { @@ -232,4 +232,127 @@ public void ExecuteAsync_GivenVariableCRLFWithNLComparedToCRNL_Success() System.Console.WriteLine(output); }); } -} \ No newline at end of file + + [TestMethod] + public void ExpectThrows_WhenExceptionIsThrown_CapturesException() + { + const string view = @"Enter a number: <>Error: Invalid input"; + + FormatException exception = ConsoleAssert.ExpectThrows(view, () => + { + System.Console.Write("Enter a number: "); + string input = System.Console.ReadLine(); + System.Console.Write("Error: Invalid input"); + int.Parse(input); // This will throw FormatException + }); + + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception); + } + + [TestMethod] + public async Task ExpectThrowsAsync_WhenExceptionIsThrown_CapturesException() + { + const string view = @"Enter a number: <>Error: Invalid input"; + + FormatException exception = await ConsoleAssert.ExpectThrowsAsync(view, async () => + { + await Task.Yield(); + System.Console.Write("Enter a number: "); + string input = System.Console.ReadLine(); + System.Console.Write("Error: Invalid input"); + int.Parse(input); // This will throw FormatException + }); + + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception); + } + + [TestMethod] + public void ExpectThrows_WhenNoExceptionIsThrown_ThrowsException() + { + const string view = @"Hello World"; + + ConsoleAssertException exception = Assert.ThrowsExactly(() => + { + ConsoleAssert.ExpectThrows(view, () => + { + System.Console.Write("Hello World"); + // No exception thrown + }); + }); + + StringAssert.Contains(exception.Message, "Expected exception of type FormatException was not thrown"); + } + + [TestMethod] + public async Task ExpectThrowsAsync_WhenNoExceptionIsThrown_ThrowsException() + { + const string view = @"Hello World"; + + ConsoleAssertException exception = await Assert.ThrowsExactlyAsync(async () => + { + await ConsoleAssert.ExpectThrowsAsync(view, async () => + { + await Task.Yield(); + System.Console.Write("Hello World"); + // No exception thrown + }); + }); + + StringAssert.Contains(exception.Message, "Expected exception of type FormatException was not thrown"); + } + + [TestMethod] + public void ExpectThrows_WithDifferentExceptionType_ThrowsOriginalException() + { + const string view = @"Enter a number: <>Error: Invalid input"; + + // Expecting ArgumentException but FormatException is thrown + Assert.ThrowsExactly(() => + { + ConsoleAssert.ExpectThrows(view, () => + { + System.Console.Write("Enter a number: "); + string input = System.Console.ReadLine(); + System.Console.Write("Error: Invalid input"); + int.Parse(input); // This throws FormatException, not ArgumentException + }); + }); + } + + [TestMethod] + public void ExpectThrows_WithNormalizeOptions_AppliesNormalization() + { + const string view = "Hello World\n"; + + ArgumentException exception = ConsoleAssert.ExpectThrows(view, () => + { + System.Console.WriteLine("Hello World"); + throw new ArgumentException("Test exception"); + }, NormalizeOptions.NormalizeLineEndings); + + Assert.IsNotNull(exception); + StringAssert.Contains(exception.Message, "Test exception"); + } + + [TestMethod] + public void ExpectThrows_WithWrongExpectedOutput_ThrowsConsoleAssertException() + { + const string view = "Wrong output <>Error: Invalid input"; + + Assert.ThrowsExactly(() => + { + ConsoleAssert.ExpectThrows(view, () => + { + System.Console.Write("Enter a number: "); + string input = System.Console.ReadLine(); + System.Console.Write("Error: Invalid input"); + int.Parse(input); + }); + }); + } +} diff --git a/IntelliTect.TestTools.Console/ConsoleAssert.cs b/IntelliTect.TestTools.Console/ConsoleAssert.cs index 7d23f31..1606bd2 100644 --- a/IntelliTect.TestTools.Console/ConsoleAssert.cs +++ b/IntelliTect.TestTools.Console/ConsoleAssert.cs @@ -166,7 +166,7 @@ public static void Expect(string expected, Func func, T expected if (!expectedReturn.Equals(@return)) { - throw new Exception($"The value returned from {nameof(func)} ({@return}) was not the {nameof(expectedReturn)}({expectedReturn}) value."); + throw new ConsoleAssertException($"The value returned from {nameof(func)} ({@return}) was not the {nameof(expectedReturn)}({expectedReturn}) value."); } } @@ -468,7 +468,7 @@ private static void AssertExpectation(string expectedOutput, string output, Func bool failTest = !areEquivalentOperator(expectedOutput, output); if (failTest) { - throw new Exception(GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage)); + throw new ConsoleAssertException(GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage)); } } @@ -721,4 +721,72 @@ public static Process ExecuteProcess(string expected, string fileName, string ar AssertExpectation(expected, standardOutput, (left, right) => LikeOperator(left, right), "The values are not like (using wildcards) each other"); return process; } -} \ No newline at end of file + + /// + /// Performs a unit test on a console-based method that is expected to throw an exception. + /// A "view" of what a user would see in their console is provided as a string, + /// where their input (including line-breaks) is surrounded by double + /// less-than/greater-than signs, like so: "Input please: <<Input>>" + /// + /// The type of exception expected to be thrown + /// Expected "view" to be seen on the console, + /// including both input and output + /// Method to be run that is expected to throw an exception + /// Options to normalize input and expected output + /// The exception that was thrown + public static TException ExpectThrows(string expected, + Action action, + NormalizeOptions normalizeOptions = NormalizeOptions.Default) + where TException : Exception + { + (string input, string expectedOutput) = Parse(expected); + TException caughtException = null; + + string output = Execute(input, () => + { + try { action(); } + catch (TException ex) { caughtException = ex; } + }); + + if (caughtException is null) + throw new ConsoleAssertException($"Expected exception of type {typeof(TException).Name} was not thrown."); + + CompareOutput(output, expectedOutput, normalizeOptions, (l, r) => l == r, "Values are not equal"); + + return caughtException; + } + + /// + /// Performs a unit test on a console-based async method that is expected to throw an exception. + /// A "view" of what a user would see in their console is provided as a string, + /// where their input (including line-breaks) is surrounded by double + /// less-than/greater-than signs, like so: "Input please: <<Input>>" + /// + /// The type of exception expected to be thrown + /// Expected "view" to be seen on the console, + /// including both input and output + /// Async method to be run that is expected to throw an exception + /// Options to normalize input and expected output + /// The exception that was thrown + public static async Task ExpectThrowsAsync(string expected, + Func action, + NormalizeOptions normalizeOptions = NormalizeOptions.Default) + where TException : Exception + { + (string input, string expectedOutput) = Parse(expected); + TException caughtException = null; + + string output = await ExecuteAsync(input, async () => + { + try { await action(); } + catch (TException ex) { caughtException = ex; } + }); + + if (caughtException is null) + throw new ConsoleAssertException($"Expected exception of type {typeof(TException).Name} was not thrown."); + + CompareOutput(output, expectedOutput, normalizeOptions, (l, r) => l == r, "Values are not equal"); + + return caughtException; + } +} diff --git a/IntelliTect.TestTools.Console/ConsoleAssertException.cs b/IntelliTect.TestTools.Console/ConsoleAssertException.cs new file mode 100644 index 0000000..1b60f77 --- /dev/null +++ b/IntelliTect.TestTools.Console/ConsoleAssertException.cs @@ -0,0 +1,14 @@ +namespace IntelliTect.TestTools.Console; + +/// +/// Exception thrown when a assertion fails. +/// +public sealed class ConsoleAssertException : Exception +{ + /// + public ConsoleAssertException(string message) : base(message) { } + + /// + public ConsoleAssertException(string message, Exception innerException) + : base(message, innerException) { } +}