非常轻量级的控制台模式测试平台






4.33/5 (2投票s)
一个库,帮助开发人员在集成到更大的UI项目之前,在控制台模式下测试核心业务操作。
动机
有时,在生产环境中,测试核心业务功能需要花费很长时间和宝贵的时间。
幸运的是,有许多平台可以简化和自动化测试。然而,这种操作并非总能提供预期的结果。
有时,操作只需要人工干预和人工评估。
此外,在大型产品中,在编译数十个库时,每一次更改都可能非常耗时,需要大量的努力,而自动测试将不会有预期的行为。
OBP 控制台测试框架是一个非常简单轻量级的库,用于测试您的业务对象、它们的行为以及复杂操作的结果,而无需在图形环境(如 Windows Forms 或其他重量级环境,如 Web、Silverlight 或 WPF)中进行。
OBP 控制台测试库的主要目标是在将核心业务功能集成到相关重量级项目中之前,在控制台模式下进行验证。
结构和需求
测试类的结构必须允许以下行为:
- 使用自定义属性和反射在运行时发现测试类和测试方法。
- 为测试人员提供一个基于控制台菜单的简单用户界面。
- 允许使用重叠菜单。
- 方便显示业务对象的集合并从中选择单个对象。
定义
- 测试方法是执行一个操作以验证开发项目中某个过程、操作或行为的方法。
- 测试类是包含一个或多个测试方法的类。它也可以包含其他测试类作为成员,以执行重叠测试。
- 菜单是显示在控制台中的一组操作,允许测试人员通过键盘调用给定的测试方法。
架构
整个库都建立在一个简单的类上:CustomConsoleTest
。该类完成以下操作:
- 它使用反射遍历后代类中的所有方法,查找具有“
TestMethod
”属性的方法。 - 使用此属性,它提取方法的显示名称以在菜单中使用。
它简化了显示集合和选择集合中的项目。 - 要运行测试,您需要实例化一个
CustomConsoleTest
的后代(此类是抽象
的)并调用“Execute
”方法。
这个简单类中的神奇之处在于,一个测试类可以嵌入其他测试类,并且父类的一个测试方法的执行可以调用子类的 Execute
方法。因此,您将构建一个非常快速的、分层的控制台菜单,用于在集成之前测试核心业务功能。
实现
所有测试类都必须派生自 CustomConsoleTest
。一切都在构造函数中。
public CustomConsoleTest()
{
var t = this.GetType();
int i = 0;
string name = null;
// find the methods
foreach (var m in t.GetMethods(BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Instance))
{
if (IsValidMethod(m, ref name))
{
var item = new MethodItem(name, m);
_methods.Add(_menuKeys[i], item);
++i;
}
}
}
当实例化一个 CustomConsoleTest
后代时,它执行以下操作:
- 使用反射,它查找具有自定义属性“
TestMethod
”的方法。 - 当一个方法具有此属性时,它会被添加到名为“
MethodItem
”的类的集合中,该类包含methodInfo
实例和方法的显示(用户友好)名称。
测试方法必须是无参数方法。
一旦发现测试方法,测试类的理念就是显示菜单,允许通过键盘按键调用测试方法。
为此,测试人员必须调用“Execute
”方法,该方法会遍历方法集合并使用属性中找到的友好名称显示菜单。
为了实现重叠测试,以及因此重叠菜单,测试类可以嵌入其他测试类,并且对其中一个测试方法的调用将执行对嵌入类“Execute
”方法的调用。
public void Execute()
{
char c = '*';
while ((c != 'q') && (c != 'Q'))
{
foreach (var p in _methods)
Console.WriteLine("
{0} - {1}", p.Key, p.Value.DisplayText);
Console.WriteLine("q - Quit");
Console.WriteLine();
c = Console.ReadKey(true).KeyChar;
if (_methods.ContainsKey(c))
try
{
var m = _methods[c];
m.Method.Invoke(this, null);
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("An exception has
occurred when invoking the method {0}",
ex);
}
}
}
对于显示集合和选择项目,我添加了两个泛型函数来简化集合的处理。这假定集合实现了泛型接口 IEnumerable<T>
。
protected void DisplayCollection<T>(IEnumerable<T> aCollection)
{
if (aCollection.Count() == 0)
Console.WriteLine("No item in the collection");
int i = 1;
_elts.Clear();
foreach (var e in aCollection)
{
Console.WriteLine("{0} - {1}", i, GetDescription(e));
_elts.Add(i, e);
++i;
}
Console.WriteLine();
}
SelectItem
方法使在控制台中选择项目更加容易。
protected T SelectItem<T>(IEnumerable<T> aCollection) where T : class
{
DisplayCollection<T>(aCollection);
while (true)
{
Console.WriteLine("Type the element number or q to exit");
var text = Console.ReadLine();
if ((text == "q") || (text == "Q"))
return null;
try
{
int index = Convert.ToInt32(text);
if (_elts.ContainsKey(index))
return (T)_elts[index];
else
Console.WriteLine("The list does not contain that number, try again !");
}
catch
{
Console.WriteLine("Wrong value");
}
}
}
为了显示菜单,我将找到的测试方法存储在一个名为 MethodItem
的类中。
class MethodItem
{
internal MethodItem(string aDisplayText, MethodInfo aMethod)
{
DisplayText = aDisplayText;
Method = aMethod;
}
internal string DisplayText { get; private set; }
internal MethodInfo Method { get; private set; }
}
示例
为了让所有这些在一个小示例中运行,应用程序包括两种业务对象类型:Machine
和 SparePart
。
class Machine : Asset
{
internal void AddPart(SparePart aPart)
{
Parts.Add(aPart);
AddWeight(aPart.Weight);
}
public Machine(string aName)
: base(aName)
{
Parts = new List<SparePart>();
}
public static void CreateInstances()
{
Machines = new List<Machine>();
for (int i = 0; i < new Random().Next(10) + 1; i++)
Machines.Add(new Machine(string.Format("Machine{0}", i)));
}
public static List<Machine> Machines
{
get;
private set;
}
public List<SparePart> Parts { get; private set; }
}
class SparePart : Asset
{
public SparePart(Machine aMachine, string aName, double aWeight):
base(aName)
{
Machine = aMachine;
Weight = aWeight;
aMachine.AddPart(this);
}
public static void CreateInstances()
{
Parts = new List<SparePart>();
var random = new Random();
foreach(var m in Machine.Machines)
for (int i = 0; i < random.Next(5); i++)
{
var part = new SparePart(m, string.Format
("{0}-part{1}", m.Name, i),
random.NextDouble());
Parts.Add(part);
}
}
public Machine Machine { get; private set; }
public static List<SparePart> Parts
{ get; private set; }
}
为了实现我的测试操作,我需要三个类:MachineTest
用于测试机器,PartTest
用于测试备件,最后是 SoftwareTest
,它嵌入了这两个测试。
代码如下
/// <summary>
/// this class is used to test software
/// </summary>
class SoftwareTest : CustomConsoleTest
{
/// <summary>
/// machine test
/// </summary>
private MachineTest _machineTest = new MachineTest();
/// <summary>
/// part test
/// </summary>
private PartTest _partTest = new PartTest();
[TestMethod("Machines")]
private void ExecuteMachineTest()
{
_machineTest.Execute();
}
[TestMethod("Parts")]
private void ExecutePartTest()
{
_partTest.Execute();
}
static SoftwareTest()
{
Machine.CreateInstances();
SparePart.CreateInstances();
}
}
class MachineTest : CustomConsoleTest
{
[TestMethod("List Machines")]
private void ListMachines()
{
DisplayCollection<Machine>(Machine.Machines);
}
protected override string GetDescription(object e)
{
if(e is Machine)
{
var m = e as Machine;
return string.Format("Machine {0} | {1:0.00} | Parts : {2}",
m.Name, m.Weight, m.Parts.Count);
}
return base.GetDescription(e);
}
}
class PartTest : CustomConsoleTest
{
protected override string GetDescription(object e)
{
if (e is SparePart)
{
var p = e as SparePart;
return string.Format("Machine :
{0} - {1}", p.Machine.Name, p.Name);
}
return base.GetDescription(e);
}
/// <summary>
/// selection
/// </summary>
[TestMethod("Select A Part")]
protected void TestSelect()
{
var p = SelectItem<SparePart>(SparePart.Parts);
if (p != null)
Console.WriteLine("You have selected the part {0}", p.Name);
}
}
为了使对象显示更精细,我覆盖了 GetDescription
方法,该方法基于 ToString
方法。
结论
根据我开发大型和长期软件系统的经验,自动化测试是好的,但还不够,而且在测试非常特殊的情况下,通常非常耗时。
我们的方法是获取软件的关键非 UI 部分,并在控制台项目中对其进行测试,以加快进程。
为了加速我们的方法,我们开发了这个库,它允许我们通过显示菜单和调用所需方法来非常快速地执行测试。
“特殊场景”必须写在后继测试类的方法中。
历史
- 2010年1月31日:初始发布