#1718 TestConsole can now be configured and accessed in CommandAppTester (#1803)

* TestConsole can now be configured and accessed in CommandAppTester
* Add test with mocked user inputs for interactive command
* Add documentation for using the CommandAppTester

Co-authored-by: Patrik Svensson <patriksvensson@users.noreply.github.com>
Co-authored-by: Marek Magath <Marek.Magath@solarwinds.com>
This commit is contained in:
Marek 2025-04-14 10:38:03 +02:00 committed by GitHub
parent 6105ee2a86
commit 57dd8ee410
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 218 additions and 11 deletions

View File

@ -63,6 +63,90 @@ The following example validates the exit code and terminal output of a `Spectre.
}
```
The following example demonstrates how to mock user inputs for an interactive command.
This test (InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput) simulates user interactions by pushing predefined inputs to the console, then verifies that the resulting output is as expected.
```csharp
public sealed class InteractiveCommandTests
{
private sealed class InteractiveCommand : Command
{
private readonly IAnsiConsole _console;
public InteractiveCommand(IAnsiConsole console)
{
_console = console;
}
public override int Execute(CommandContext context)
{
var fruits = _console.Prompt(
new MultiSelectionPrompt<string>()
.Title("What are your [green]favorite fruits[/]?")
.NotRequired() // Not required to have a favorite fruit
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a fruit, " +
"[green]<enter>[/] to accept)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Coconut",
}));
var fruit = _console.Prompt(
new SelectionPrompt<string>()
.Title("What's your [green]favorite fruit[/]?")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut",
}));
var name = _console.Ask<string>("What's your name?");
_console.WriteLine($"[{string.Join(',', fruits)};{fruit};{name}]");
return 0;
}
}
[Fact]
public void InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput()
{
// Given
TestConsole console = new();
console.Interactive();
// Your mocked inputs must always end with "Enter" for each prompt!
// Multi selection prompt: Choose first option
console.Input.PushKey(ConsoleKey.Spacebar);
console.Input.PushKey(ConsoleKey.Enter);
// Selection prompt: Choose second option
console.Input.PushKey(ConsoleKey.DownArrow);
console.Input.PushKey(ConsoleKey.Enter);
// Ask text prompt: Enter name
console.Input.PushTextWithEnter("Spectre Console");
var app = new CommandAppTester(null, new CommandAppTesterSettings(), console);
app.SetDefaultCommand<InteractiveCommand>();
// When
var result = app.Run();
// Then
result.ExitCode.ShouldBe(0);
result.Output.EndsWith("[Apple;Apricot;Spectre Console]");
}
}
```
## Testing console behaviour
`TestConsole` and `TestConsoleInput` are testable implementations of `IAnsiConsole` and `IAnsiConsoleInput`, allowing you fine-grain control over testing console output and interactivity.

View File

@ -8,6 +8,11 @@ public sealed class CommandAppTester
private Action<CommandApp>? _appConfiguration;
private Action<IConfigurator>? _configuration;
/// <summary>
/// Gets the test console used by both the CommandAppTester and CommandApp.
/// </summary>
public TestConsole Console { get; }
/// <summary>
/// Gets or sets the Registrar to use in the CommandApp.
/// </summary>
@ -23,10 +28,15 @@ public sealed class CommandAppTester
/// </summary>
/// <param name="registrar">The registrar.</param>
/// <param name="settings">The settings.</param>
public CommandAppTester(ITypeRegistrar? registrar = null, CommandAppTesterSettings? settings = null)
/// <param name="console">The test console that overrides the default one.</param>
public CommandAppTester(
ITypeRegistrar? registrar = null,
CommandAppTesterSettings? settings = null,
TestConsole? console = null)
{
Registrar = registrar;
TestSettings = settings ?? new CommandAppTesterSettings();
Console = console ?? new TestConsole().Width(int.MaxValue);
}
/// <summary>
@ -36,6 +46,7 @@ public sealed class CommandAppTester
public CommandAppTester(CommandAppTesterSettings settings)
{
TestSettings = settings;
Console = new TestConsole().Width(int.MaxValue);
}
/// <summary>
@ -85,25 +96,23 @@ public sealed class CommandAppTester
public CommandAppFailure RunAndCatch<T>(params string[] args)
where T : Exception
{
var console = new TestConsole().Width(int.MaxValue);
try
{
Run(args, console, c => c.PropagateExceptions());
Run(args, Console, c => c.PropagateExceptions());
throw new InvalidOperationException("Expected an exception to be thrown, but there was none.");
}
catch (T ex)
{
if (ex is CommandAppException commandAppException && commandAppException.Pretty != null)
{
console.Write(commandAppException.Pretty);
Console.Write(commandAppException.Pretty);
}
else
{
console.WriteLine(ex.Message);
Console.WriteLine(ex.Message);
}
return new CommandAppFailure(ex, console.Output);
return new CommandAppFailure(ex, Console.Output);
}
catch (Exception ex)
{
@ -120,8 +129,7 @@ public sealed class CommandAppTester
/// <returns>The result.</returns>
public CommandAppResult Run(params string[] args)
{
var console = new TestConsole().Width(int.MaxValue);
return Run(args, console);
return Run(args, Console);
}
private CommandAppResult Run(string[] args, TestConsole console, Action<IConfigurator>? config = null)
@ -164,8 +172,7 @@ public sealed class CommandAppTester
/// <returns>The result.</returns>
public async Task<CommandAppResult> RunAsync(params string[] args)
{
var console = new TestConsole().Width(int.MaxValue);
return await RunAsync(args, console);
return await RunAsync(args, Console);
}
private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null)

View File

@ -44,4 +44,40 @@ public sealed class CommandAppTesterTests
// Then
result.Output.ShouldBe(expected);
}
[Fact]
public void DefaultCtor_WithoutParameters_CreatesDefaultConsole()
{
// Given, When
CommandAppTester app = new();
// Then
app.Console.ShouldNotBeNull();
app.Console.Profile.Width.ShouldBe(int.MaxValue);
}
[Fact]
public void DefaultCtor_WithCustomConsole_UsesProvidedInstance()
{
// Given
TestConsole console = new();
// When
CommandAppTester app = new(null, new CommandAppTesterSettings(), console);
// Then
app.Console.ShouldNotBeNull();
app.Console.ShouldBeSameAs(console);
}
[Fact]
public void Ctor_WithSettings_CreatesDefaultConsole()
{
// Given, When
CommandAppTester app = new(new CommandAppTesterSettings());
// Then
app.Console.ShouldNotBeNull();
app.Console.Profile.Width.ShouldBe(int.MaxValue);
}
}

View File

@ -0,0 +1,80 @@
namespace Spectre.Console.Cli.Tests.Unit.Testing;
public sealed class InteractiveCommandTests
{
private sealed class InteractiveCommand : Command
{
private readonly IAnsiConsole _console;
public InteractiveCommand(IAnsiConsole console)
{
_console = console;
}
public override int Execute(CommandContext context)
{
var fruits = _console.Prompt(
new MultiSelectionPrompt<string>()
.Title("What are your [green]favorite fruits[/]?")
.NotRequired() // Not required to have a favorite fruit
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a fruit, " +
"[green]<enter>[/] to accept)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Coconut",
}));
var fruit = _console.Prompt(
new SelectionPrompt<string>()
.Title("What's your [green]favorite fruit[/]?")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut",
}));
var name = _console.Ask<string>("What's your name?");
_console.WriteLine($"[{string.Join(',', fruits)};{fruit};{name}]");
return 0;
}
}
[Fact]
public void InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput()
{
// Given
TestConsole console = new();
console.Interactive();
// Your mocked inputs must always end with "Enter" for each prompt!
// Multi selection prompt: Choose first option
console.Input.PushKey(ConsoleKey.Spacebar);
console.Input.PushKey(ConsoleKey.Enter);
// Selection prompt: Choose second option
console.Input.PushKey(ConsoleKey.DownArrow);
console.Input.PushKey(ConsoleKey.Enter);
// Ask text prompt: Enter name
console.Input.PushTextWithEnter("Spectre Console");
var app = new CommandAppTester(null, new CommandAppTesterSettings(), console);
app.SetDefaultCommand<InteractiveCommand>();
// When
var result = app.Run();
// Then
result.ExitCode.ShouldBe(0);
result.Output.EndsWith("[Apple;Apricot;Spectre Console]");
}
}