如何提高命令解析器的可测试性(在 C# 控制台应用程序中)?

How to improve testability of a command parser (in a C# Console app)?

我有一个程序使用控制台作为 C# .NET 中的 GUI 来解析来自用户的命令。它有不同的命令 - 其中一些必须完全匹配,如 "look"、"inventory" 或 "help"。其他只需要包含部分单词或短语 - 任何带有 "north" 或 "east" 的短语都会在世界上向该方向发起移动。

例如:

if(command == "help")
  { << Console.Writeline code to print the help >> }
else if (command.Contains == "inv")
  {  << code using Console.Writeline to print the inventory >> )
else if (command.Contains("north"))
  { << code to move north, then print location info with Console.Writeline >>)
<< etc. >>

因为它是一个控制台应用程序,所以很多操作代码作为输出写入控制台。我试图弄清楚如何对此进行单元测试,而我(公认的初学者)认为我应该删除对控制台的依赖并使用依赖注入来传递控制台(或者可能是文本流的通用接口或类似的东西?)到这个解析代码中,这样我就可以伪造控制台,但我不确定该怎么做。

问题

依赖注入是否是执行此操作的正确方法 - 如果是,实施它的正确方法是什么?

Since it's a console app, a lot of the action code writes to the console as output. I'm trying to figure out how to unit test this

一种方法是使用 TraceListener 并将所有内容记录到文件而不是控制台。通常我们使用 TextWriterTraceListenerTraceDebug 输出记录到文件中。

[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[TestClass]
public class AssemblyInitUnitTest
{
    static FileStream objStream;

    [AssemblyInitialize()]
    public static void Setup(TestContext testContext)
    {
        objStream = new FileStream(AppDomain.CurrentDomain.BaseDirectory + "\AAA_UnitTestPerfMonitor.txt", FileMode.OpenOrCreate);
        TextWriterTraceListener objTraceListener = new TextWriterTraceListener(objStream);
        Trace.Listeners.Add(objTraceListener);
        Trace.WriteLine("===================================");
        Trace.WriteLine("App Start:" + DateTime.Now);
        Trace.WriteLine("===================================");    
    }

    [AssemblyCleanup]
    public static void TearDown()
    {
        Trace.Flush();
        objStream.Close();
    }
}

我们可以为控制台做同样的事情,像这样在 [AssemblyInitialize()] 中连接它:

ConsoleTraceListener ctl = new ConsoleTraceListener(false);
ctl.TraceOutputOptions = TraceOptions.DateTime;
Trace.Listeners.Add(ctl);

然后就可以读取文件了,Assert实际结果和预期的结果一样。

string[] fileLines = System.IO.File.ReadAllLines(AppDomain.CurrentDomain.BaseDirectory + "\AAA_UnitTestPerfMonitor.txt");
Assert.IsTrue(fileLines[0] == "<< Console.Writeline code to print the help >> ");

可能还有其他方法。所以闲逛一下,看看是否还有其他人回答。

在这里你不必在一个地方写你的逻辑。您可以利用命令模式。您将需要一个代表对象状态的 class。我们将其称为 CustomObject。

public class CustomObject
{
    //properties that represent the state, direction, inventory, etc.
    public string Direction{get;set;}//etc.
}

public interface ICommand
{
    string Execute(CustomObject obj);
}

public class InventoryCommand: ICommand
{
    public string Execute(CustomObject obj)
    {
        //code to create the inventory string from CustomObject
        return "Inventory String";
    }
}

public class NorthCommand: ICommand
{
    public string Execute(CustomObject obj)
    {
        //code to move the object to north
        return "Command Information";
    }
}


//In your test cases, you can do

CustomObject obj = new CustomObject();
//test for inventory command
var expectedOutput = "Expected Output";
var result = (new InventoryCommand()).Execute(obj);
Assert.Equal(result, expectedOuput);

//In your console program
if(command == "help")
{  
    Console.Writeline((new HelpCommand()).Execute(obj)); 
}
else if (command.Contains == "inv")
{  
    Console.Writeline((new InventoryCommand()).Execute(obj)); 
)

您可以根据您拥有的不同命令系列进一​​步隔离命令界面。

考虑到 testing pyramid 你应该:

集成测试

从使用脚本语言进行测试的集成测试盒测试开始,很少并且应该是探索性的或涉及涉及标准 input/output 的极端情况。

$process = New-Object System.Diagnostics.Process
$process.StartInfo.FileName = ".\mud.exe"
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardInput = $true
if ( $process.Start() ) {
    # input
    $process.StandardInput.WriteLine("help");
    $process.StandardInput.WriteLine();
    # output check
    $output = $process.StandardOutput.ReadToEnd()
    if ( $output ) {
        if ( $output.Contains("this is a help") ) {
            Write "pass"
        }
        else {
            Write-Error $output
        }
    }
    $process.WaitForExit()
}

输入

使用像 fluent command line parser

这样的库快速安排输入验证
[Flags]
enum Commands
{
    Help = 1,
    Inv = 2,
    North = 4
}
var p = new FluentCommandLineParser();
p.Setup<Commands>("c")
 .Callback(c => command= c);

输出

注入并组合你的输出,这样你就可以大量单元测试而无需太多模拟。
这意味着所有控制台写入都将由一个模块处理,您可以通过测试套件轻松伪造该模块。

IConsoleBuilder { // actual implementation write to console
    RegisterCommand(string command, Func<string[], string> action); 
}

InventoryConsoleBuilder : ConsoleBuilderClient { 
    InventoryConsoleBuilder(IConsoleWriter writer){ _writer = writer; } 

    public override void Show(IInventory inventory) { 
        writer.RegisterCommand(inventoryComposed) ; 
    }
}