如何提高命令解析器的可测试性(在 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 并将所有内容记录到文件而不是控制台。通常我们使用 TextWriterTraceListener
将 Trace
和 Debug
输出记录到文件中。
[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) ;
}
}
我有一个程序使用控制台作为 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 并将所有内容记录到文件而不是控制台。通常我们使用 TextWriterTraceListener
将 Trace
和 Debug
输出记录到文件中。
[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) ;
}
}