Learning Reflection c#- Create your own test Runner and Framework

Khemlall Mangal
4 min readJan 4, 2023

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);
}
}

--

--

Khemlall Mangal

I am a passionate coder, QA Engineer, and someone who enjoys the outdoors.