Learning Reflection c#- Create your own test Runner and Framework
Hey, Guys ever wonder how to create your own automation framework and test runner? Is that something that interests you? Let's do just that today. As a note, there is many many open-source testing frameworks available and chances are you do not need to create your own. But this will teach us a lot about reflection and how to use them. If you are curious how it all works lets dive in!
What will we be building? We will build a test runner and test framework that will run your test and provide a report of the test run.
Tools: VS STUDIO, C#
Step 1. Lets create a Test runner. Create a console application call it myTestRunner
on the program.cs we will add the following
namespace MyTestRunner
{
using Tests;
public class Program
{
public static void Main()
=> TestRunner.ExecuteTests(typeof(Car));
}
}
Create another file call ITestReporter.cs
namespace MyTestRunner
{
public interface ITestReporter
{
void Report(string message = null);
void ReportLine(string message = null);
}
}
Create another file call StringExtentions.cs
namespace MyTestRunner
{
using System;
using System.Linq;
public static class StringExtensions
{
public static string Capitalize(this string input) =>
input switch
{
null => throw new ArgumentNullException(nameof(input)),
"" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),
_ => input.First().ToString().ToUpper() + input.Substring(1)
};
}
}
ConsoleReporter.cs
namespace MyTestRunner
{
using System;
public class ConsoleReporter : ITestReporter
{
public void Report(string message = null)
=> Console.Write(message);
public void ReportLine(string message = null)
=> Console.WriteLine(message);
}
}
TestRunner.cs
namespace MyTestRunner
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Framework;
public class TestRunner
{
private static int totalTests;
private static int passingTests;
private static int failedTests;
private static int testErrors;
public static ITestReporter TestReporter { get; set; } = new ConsoleReporter();
public static void ExecuteTests(params Type[] types)
{
var assemblies = types.Select(t => t.Assembly).ToArray();
var testsBySubject = FindTests(assemblies)
.GroupBy(t => t
.GetCustomAttribute<SubjectAttribute>()
.Name);
TestReporter.ReportLine("Running tests...");
foreach (var tests in testsBySubject)
{
var testSubject = tests.Key;
TestReporter.ReportLine($"--Running tests for '{testSubject}'...");
foreach (var test in tests)
{
var testComponents = test.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
var givenComponents = GetTestComponents(testComponents, typeof(Given));
var becauseComponents = GetTestComponents(testComponents, typeof(Because));
var itComponents = GetTestComponents(testComponents, typeof(It));
var testInstance = Activator.CreateInstance(test);
try
{
RunGivenComponents(givenComponents, testInstance);
RunBecauseComponents(becauseComponents, testInstance);
RunItComponents(itComponents, testInstance, test.Name);
}
catch (Exception exception)
{
TestReporter.ReportLine();
TestReporter.ReportLine("Unhandled exception in test code!");
TestReporter.ReportLine(exception.ToString());
TestReporter.ReportLine();
testErrors++;
}
}
}
TestReporter.ReportLine();
TestReporter.ReportLine(new string('-', 50));
TestReporter.ReportLine($"Total Tests: {totalTests}");
TestReporter.ReportLine($"Passing Tests: {passingTests}");
TestReporter.ReportLine($"Failed Tests: {failedTests}");
if (testErrors > 0)
{
TestReporter.ReportLine($"Tests With Errors: {testErrors}");
}
}
private static IEnumerable<Type> FindTests(params Assembly[] assemblies)
=> assemblies
.SelectMany(a => a.ExportedTypes)
.Where(t => t.IsDefined(typeof(SubjectAttribute)))
.ToList();
private static List<FieldInfo> GetTestComponents(
IEnumerable<FieldInfo> fields,
Type typeOfComponent)
{
var components = fields
.Where(f => f.FieldType == typeOfComponent)
.ToList();
if (!components.Any())
{
throw new InvalidOperationException($"Test does not contain {typeOfComponent.Name} specification.");
}
return components;
}
private static void RunGivenComponents(
IEnumerable<FieldInfo> testComponents,
object test)
{
var values = GetComponents<Given>(testComponents, test);
foreach (var value in values)
{
value.Invoke();
}
}
private static void RunBecauseComponents(
IEnumerable<FieldInfo> testComponents,
object test)
{
var values = GetComponents<Because>(testComponents, test);
foreach (var value in values)
{
value.Invoke();
}
}
private static void RunItComponents(
IEnumerable<FieldInfo> testComponents,
object test,
string testPrefix)
{
var its = testComponents
.Select(tc => new
{
Name = tc.Name,
Value = (It)tc.GetValue(test)
});
foreach (var it in its)
{
totalTests++;
TestReporter.Report($"----Running {testPrefix}It{it.Name.Capitalize()} - ");
try
{
it.Value.Invoke();
TestReporter.ReportLine("Passing");
passingTests++;
}
catch (Exception exception)
{
TestReporter.ReportLine("Failed");
TestReporter.ReportLine();
TestReporter.ReportLine($"Exception Message: {exception.Message}");
TestReporter.ReportLine();
failedTests++;
}
}
}
private static IEnumerable<TComponent> GetComponents<TComponent>(
IEnumerable<FieldInfo> testComponents,
object test)
=> testComponents
.Select(tc => tc.GetValue(test))
.Cast<TComponent>();
}
}
Create a Class project inside this new project MyTestRunner.Framework
SubjectAttribute.cs
namespace MyTestRunner.Framework
{
using System;
[AttributeUsage(AttributeTargets.Class)]
public class SubjectAttribute : Attribute
{
public SubjectAttribute(string name) => this.Name = name;
public string Name { get; }
}
}
TestComponents.cs
namespace MyTestRunner.Framework
{
public delegate void Given();
public delegate void Because();
public delegate void It();
}
Create a New class for your Test class project
cars
namespace MyTestRunner.Tests
{
public class Car
{
public string Model { get; private set; }
public bool IsRunning { get; private set; }
public void Produce(string model) => this.Model = model;
public void Start() => this.IsRunning = true;
public void Stop() => this.IsRunning = false;
}
}
WhenCar.cs
namespace MyTestRunner.Tests
{
using Framework;
using Shouldly;
[Subject("Car")]
public class WhenCarIsStarted
{
const string Model = "BMW";
static Car car;
Given context = () =>
{
car = new Car();
car.Produce(Model);
};
Because of = () => car.Start();
It shouldBeRunning = () => car.IsRunning.ShouldBe(true);
It shouldHaveCorrectModel = () => car.Model.ShouldBe(Model);
}
[Subject("Car")]
public class WhenCarIsStopped
{
static Car car;
Given context = () => car = new Car();
Because of = () => car.Stop();
It shouldBeRunning = () => car.IsRunning.ShouldBe(false);
}
}