Motivations
Sometimes, in production environment, testing core business functions needs long and precious time.
Fortunately, there are a lot of platforms that simplify and automate testing. However, this operation cannot always provide the expected results.
Sometimes, operations need simply to be checked by human intervention and evaluated by human appreciation.
Moreover, in large products, when compiling dozens of libraries, every change can be very time consuming and require a lot of effort and automatic testing will not have the expected behavior.
The OBP console testing framework is a very simple and lightweight library that is used to test your business objects, their behaviors and the result of complex operations without being in a graphical environment such as Windows Forms, or another heavy environment, such as web, Silverlight or WPF.
The main goal of the OBP Console Test library is to validate core business functions in console mode before integrating them in the related heavy project.
Structure and Needs
The structure of the test class has to permit the following behavior:
- Discover test classes and test methods at runtime using custom attributes and reflection
- Provide the tester a simple user interface based on console menus
- Allow the usage of overlapping menus
- Facilitate displaying collections of business objects and selecting individual objects from them
Definitions
- A test method is a method that performs an action to validate a process, an action or a behavior that is part of a development project.
- A test class is a class that contains one or more test methods. It could also contain other test classes as members to perform overlapped tests.
- A menu is a set of actions displayed in console that allow the tester to invoke a given test method using keyboard.
Architecture
The whole library is built on one simple class: CustomConsoleTest
. The class accomplishes the following actions:
- It browses to all the methods of the descendant classes using reflections looking for methods that have the attribute “
TestMethod
”.
- Using this attribute, it extracts the display name of the method to use it in the menu.
It simplifies displaying collections and selecting an item within a collection.
- To make the test run, you need to instantiate a
CustomConsoleTest
descendant (this class is abstract
) and call the method “Execute
”.
The magic in this simple class is that a test class can embed other test classes and the execution of one of the test methods of the parent class could call the Execute
method of the child class. By consequence, you'll build a very quick hierarchical console menu dedicated to test core business functions before integration.
Implementation
All the test classes have to derive from CustomConsoleTest
. All is in the constructor:
public CustomConsoleTest()
{
var t = this.GetType();
int i = 0;
string name = null;
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;
}
}
}
When a CustomConsoleTest
descendant is instantiated, it performs the following actions:
- Using reflection, it looks for methods that have the custom attribute «
TestMethod
»
- When a method has this attribute, it is added to a collection of a class called «
MethodItem
» that contains the methodInfo
instance and the display (user friendly) name of the method.
The test methods have to be parameterless methods.
Once the test method is discovered, the idea behind the test class is to display menus that allow invoking test methods through keyboard keys.
To accomplish that, the tester has to call the method "Execute
" that browses the method collection and displays a menu using the friendly name found in the attribute.
To put in overlapped tests and consequently overlapped menu, test classes could embed other test classes and the call of one of the test methods will perform a call to the "Execute
" method of the embedded classes.
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);
}
}
}
For displaying collections and selecting items, I added to generic functions that simplify handling collections. That supposes that the collection implements the generic interface 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();
}
The method SelectItem
makes selecting items in console mode easier:
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");
}
}
}
To display menus, I store the found test methods in a class called 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; }
}
Example
To make all this run in a small example, the application includes two business object types: Machine
and 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; }
}
To implement my test operations, I need three classes: MachineTest
for testing machine, PartTest
for testing spare parts and finally SoftwareTest
that embeds the two tests.
The code is as follows:
class SoftwareTest : CustomConsoleTest
{
private MachineTest _machineTest = new MachineTest();
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);
}
[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);
}
}
To make displaying objects finer, I override the method GetDescription
that is natively based on the ToString
method.
Conclusion
In my own experience in developing large and long software systems, automated testing is good but not enough and very often, time-consuming when testing very particular scenarios.
Our approach was to get key non-UI portions of software and test them in console projects to make things go quicker.
For accelerating our approach, we developed this library that allows us in performing tests very quickly by displaying menus and invoking required methods.
The "particular scenario" has to be written in the descending class test methods.
History
- 31st January, 2010: Initial post