Skip to content

Instantly share code, notes, and snippets.

@robdmoore
Last active February 15, 2017 10:59
Show Gist options
  • Save robdmoore/deeb72772b1c06fbbb130a8b9922e2be to your computer and use it in GitHub Desktop.
Save robdmoore/deeb72772b1c06fbbb130a8b9922e2be to your computer and use it in GitHub Desktop.
Xunit2 compatible Bddfy output reporter
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using TestStack.BDDfy;
using TestStack.BDDfy.Configuration;
namespace {SomeProject}.Tests.TestHelpers
{
// Modified from https://github.com/TestStack/TestStack.BDDfy/blob/master/src/TestStack.BDDfy/Reporters/TextReporter.cs
public class ThreadsafeBddfyTextReporter : IProcessor
{
private readonly AsyncLocal<List<Exception>> _exceptions = new AsyncLocal<List<Exception>>();
protected readonly AsyncLocal<StringBuilder> _text = new AsyncLocal<StringBuilder>();
private AsyncLocal<int> _longestStepSentence = new AsyncLocal<int>();
public virtual void Process(Story story)
{
if (_exceptions.Value == null)
_exceptions.Value = new List<Exception>();
if (_text.Value == null)
_text.Value = new StringBuilder();
ReportStoryHeader(story);
var allSteps = story.Scenarios.SelectMany(s => s.Steps)
.Select(GetStepWithLines)
.ToList();
if (allSteps.Any())
_longestStepSentence.Value = allSteps.SelectMany(s => s.Item2.Select(l => l.Length)).Max();
foreach (var scenarioGroup in story.Scenarios.GroupBy(s => s.Id))
{
if (scenarioGroup.Count() > 1)
{
// all scenarios in an example based scenario share the same header and narrative
var exampleScenario = story.Scenarios.First();
Report(exampleScenario);
if (exampleScenario.Steps.Any())
{
foreach (var step in exampleScenario.Steps.Where(s => s.ShouldReport))
ReportOnStep(exampleScenario, GetStepWithLines(step), false);
}
WriteLine();
WriteExamples(exampleScenario, scenarioGroup);
ReportTags(exampleScenario.Tags);
}
else
{
foreach (var scenario in story.Scenarios)
{
Report(scenario);
if (scenario.Steps.Any())
{
foreach (var step in scenario.Steps.Where(s => s.ShouldReport))
ReportOnStep(scenario, GetStepWithLines(step), true);
}
}
var exampleScenario = story.Scenarios.First();
ReportTags(exampleScenario.Tags);
}
}
ReportExceptions();
}
private static Tuple<Step, string[]> GetStepWithLines(Step s)
{
return Tuple.Create(s, s.Title.Replace("\r\n", "\n").Split('\n').Select(l => PrefixWithSpaceIfRequired(l, s.ExecutionOrder)).ToArray());
}
private void ReportTags(List<string> tags)
{
if (!tags.Any())
return;
WriteLine();
WriteLine("Tags: {0}", string.Join(", ", tags));
}
private void WriteExamples(Scenario exampleScenario, IEnumerable<Scenario> scenarioGroup)
{
WriteLine("Examples: ");
var scenarios = scenarioGroup.ToArray();
var allPassed = scenarios.All(s => s.Result == Result.Passed);
var exampleColumns = exampleScenario.Example.Headers.Length;
var numberColumns = allPassed ? exampleColumns : exampleColumns + 2;
var maxWidth = new int[numberColumns];
var rows = new List<string[]>();
Action<IEnumerable<string>, string, string> addRow = (cells, result, error) =>
{
var row = new string[numberColumns];
var index = 0;
foreach (var cellText in cells)
row[index++] = cellText;
if (!allPassed)
{
row[numberColumns - 2] = result;
row[numberColumns - 1] = error;
}
for (var i = 0; i < numberColumns; i++)
{
var rowValue = row[i];
if (rowValue != null && rowValue.Length > maxWidth[i])
maxWidth[i] = rowValue.Length;
}
rows.Add(row);
};
addRow(exampleScenario.Example.Headers, "Result", "Errors");
foreach (var scenario in scenarios)
{
var failingStep = scenario.Steps.FirstOrDefault(s => s.Result == Result.Failed);
var error = failingStep == null
? null
: string.Format("Step: {0} failed with exception: {1}", failingStep.Title, CreateExceptionMessage(failingStep));
addRow(scenario.Example.Values.Select(e => e.GetValueAsString()), scenario.Result.ToString(), error);
}
foreach (var row in rows)
WriteExampleRow(row, maxWidth);
}
private void WriteExampleRow(string[] row, int[] maxWidth)
{
for (int index = 0; index < row.Length; index++)
{
var col = row[index];
Write("| {0} ", (col ?? string.Empty).Trim().PadRight(maxWidth[index]));
}
WriteLine("|");
}
private void ReportStoryHeader(Story story)
{
if (story.Metadata == null || story.Metadata.Type == null)
return;
WriteLine(story.Metadata.TitlePrefix + story.Metadata.Title);
if (!string.IsNullOrEmpty(story.Metadata.Narrative1))
WriteLine("\t" + story.Metadata.Narrative1);
if (!string.IsNullOrEmpty(story.Metadata.Narrative2))
WriteLine("\t" + story.Metadata.Narrative2);
if (!string.IsNullOrEmpty(story.Metadata.Narrative3))
WriteLine("\t" + story.Metadata.Narrative3);
}
static string PrefixWithSpaceIfRequired(string stepTitle, ExecutionOrder executionOrder)
{
if (executionOrder == ExecutionOrder.ConsecutiveAssertion ||
executionOrder == ExecutionOrder.ConsecutiveSetupState ||
executionOrder == ExecutionOrder.ConsecutiveTransition)
stepTitle = " " + stepTitle; // add two spaces in the front for indentation.
return stepTitle;
}
void ReportOnStep(Scenario scenario, Tuple<Step, string[]> stepAndLines, bool includeResults)
{
if (!includeResults)
{
foreach (var line in stepAndLines.Item2)
{
WriteLine("\t{0}", line);
}
return;
}
var step = stepAndLines.Item1;
var humanizedResult = Configurator.Scanners.Humanize(step.Result.ToString());
string message;
if (scenario.Result == Result.Passed)
message = string.Format("\t{0}", stepAndLines.Item2[0]);
else
{
var paddedFirstLine = stepAndLines.Item2[0].PadRight(_longestStepSentence.Value + 5);
message = string.Format("\t{0} [{1}] ", paddedFirstLine, humanizedResult);
}
if (stepAndLines.Item2.Length > 1)
{
message = string.Format("{0}\r\n{1}", message, string.Join("\r\n", stepAndLines.Item2.Skip(1)));
}
if (step.Exception != null)
message += CreateExceptionMessage(step);
WriteLine(message);
}
private string CreateExceptionMessage(Step step)
{
_exceptions.Value.Add(step.Exception);
var exceptionReference = string.Format("[Details at {0} below]", _exceptions.Value.Count);
if (!string.IsNullOrEmpty(step.Exception.Message))
return string.Format("[{0}] {1}", FlattenExceptionMessage(step.Exception.Message), exceptionReference);
return string.Format("{0}", exceptionReference);
}
void ReportExceptions()
{
WriteLine();
if (_exceptions.Value.Count == 0)
return;
Write("Exceptions:");
for (int index = 0; index < _exceptions.Value.Count; index++)
{
var exception = _exceptions.Value[index];
WriteLine();
Write(string.Format(" {0}. ", index + 1));
if (!string.IsNullOrEmpty(exception.Message))
WriteLine(FlattenExceptionMessage(exception.Message));
else
WriteLine();
WriteLine(exception.StackTrace);
}
WriteLine();
}
static string FlattenExceptionMessage(string message)
{
return string.Join(" ", message
.Replace("\t", " ") // replace tab with one space
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
.Select(s => s.Trim()))
.TrimEnd(','); // chop any , from the end
}
void Report(Scenario scenario)
{
WriteLine();
WriteLine("Scenario: " + scenario.Title);
}
public ProcessType ProcessType
{
get { return ProcessType.Report; }
}
public override string ToString()
{
return _text.ToString();
}
protected virtual void WriteLine(string text = null)
{
_text.Value.AppendLine(text);
}
protected virtual void WriteLine(string text, params object[] args)
{
_text.Value.AppendLine(string.Format(text, args));
}
protected virtual void Write(string text, params object[] args)
{
_text.Value.AppendFormat(text, args);
}
}
}
// Somewhere global
Configurator.Processors.Add(() => Xunit2BddfyTextReporter.Instance);
// ...
public class MyTestClass {
public MyTestClass(ITestOutputHelper output) {
Xunit2BddfyTextReporter.Instance.RegisterOutput(output);
}
}
using System.Text;
using System.Threading;
using TestStack.BDDfy;
using TestStack.BDDfy.Reporters;
using Xunit.Abstractions;
namespace {SomeProject}.Tests.TestHelpers
{
public class Xunit2BddfyTextReporter : ThreadsafeBddfyTextReporter
{
public static readonly Xunit2BddfyTextReporter Instance = new Xunit2BddfyTextReporter();
private static readonly AsyncLocal<ITestOutputHelper> Output = new AsyncLocal<ITestOutputHelper>();
private static readonly List<object> Processed = new List<object>();
private Xunit2BddfyTextReporter() {}
public void RegisterOutput(ITestOutputHelper output)
{
Output.Value = output;
}
public override void Process(Story story)
{
// For some reason this gets called multiple times when tests are run in parallel
if (Processed.Contains(story))
return;
Processed.Add(story);
base.Process(story);
if (Output.Value != null)
Output.Value.WriteLine(_text.Value.ToString());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment