Skip to content

Commit

Permalink
Adds support for xUnit2 ITestOutputHelper #575 (#874)
Browse files Browse the repository at this point in the history
* Resolves #575 

* added tests for adding support for xUnit2 ITestOutputHelper class

added XUnit2Generator unit tests
added XUnit2Provider specs
updated XUnitExecutionDriver to output results in default (xUnit) xml
output instead of NUnit xml format

* swapped the order of the TestInitialize call and the setting of the _testOutputHelper field
  • Loading branch information
aptester authored and SabotageAndi committed Jun 27, 2017
1 parent 4b2ff9b commit 8bcab7f
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public class XUnit2TestGeneratorProvider : XUnitTestGeneratorProvider
private const string SKIP_REASON = "Ignored";
private const string ICLASSFIXTURE_INTERFACE = "Xunit.IClassFixture";
private const string COLLECTION_ATTRIBUTE = "Xunit.CollectionAttribute";
private const string OUTPUT_INTERFACE = "Xunit.Abstractions.ITestOutputHelper";
private const string OUTPUT_INTERFACE_PARAMETER_NAME = "testOutputHelper";
private const string OUTPUT_INTERFACE_FIELD_NAME = "_testOutputHelper";
private const string FIXTUREDATA_PARAMETER_NAME = "fixtureData";

public XUnit2TestGeneratorProvider(CodeDomHelper codeDomHelper)
:base(codeDomHelper)
Expand All @@ -29,8 +33,14 @@ public override UnitTestGeneratorTraits GetTraits()
return UnitTestGeneratorTraits.RowTests | UnitTestGeneratorTraits.ParallelExecution;
}

protected override CodeTypeReference CreateFixtureInterface(CodeTypeReference fixtureDataType)
protected override CodeTypeReference CreateFixtureInterface(TestClassGenerationContext generationContext, CodeTypeReference fixtureDataType)
{
// Add a field for the ITestOutputHelper
generationContext.TestClass.Members.Add(new CodeMemberField(OUTPUT_INTERFACE, OUTPUT_INTERFACE_FIELD_NAME));

// Store the fixture data type for later use in constructor
generationContext.CustomData.Add(FIXTUREDATA_PARAMETER_NAME, fixtureDataType);

return new CodeTypeReference(ICLASSFIXTURE_INTERFACE, fixtureDataType);
}

Expand Down Expand Up @@ -61,6 +71,20 @@ public override void SetRow(TestClassGenerationContext generationContext, CodeMe
CodeDomHelper.AddAttribute(testMethod, INLINEDATA_ATTRIBUTE, args.ToArray());
}

protected override void SetTestConstructor(TestClassGenerationContext generationContext, CodeConstructor ctorMethod) {
ctorMethod.Parameters.Add(
new CodeParameterDeclarationExpression((CodeTypeReference)generationContext.CustomData[FIXTUREDATA_PARAMETER_NAME], FIXTUREDATA_PARAMETER_NAME));
ctorMethod.Parameters.Add(
new CodeParameterDeclarationExpression(OUTPUT_INTERFACE, OUTPUT_INTERFACE_PARAMETER_NAME));

ctorMethod.Statements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), OUTPUT_INTERFACE_FIELD_NAME),
new CodeVariableReferenceExpression(OUTPUT_INTERFACE_PARAMETER_NAME)));

base.SetTestConstructor(generationContext, ctorMethod);
}

public override void SetTestMethodIgnore(TestClassGenerationContext generationContext, CodeMemberMethod testMethod)
{
var factAttr = testMethod.CustomAttributes.OfType<CodeAttributeDeclaration>()
Expand Down Expand Up @@ -92,5 +116,23 @@ public override void SetTestClassParallelize(TestClassGenerationContext generati
{
CodeDomHelper.AddAttribute(generationContext.TestClass, COLLECTION_ATTRIBUTE, new CodeAttributeArgument(new CodePrimitiveExpression(Guid.NewGuid())));
}

public override void FinalizeTestClass(TestClassGenerationContext generationContext)
{
base.FinalizeTestClass(generationContext);

// testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs<ITestOutputHelper>(_testOutputHelper);
generationContext.ScenarioInitializeMethod.Statements.Add(
new CodeMethodInvokeExpression(
new CodeMethodReferenceExpression(
new CodePropertyReferenceExpression(
new CodePropertyReferenceExpression(
new CodeFieldReferenceExpression(null, generationContext.TestRunnerField.Name),
"ScenarioContext"),
"ScenarioContainer"),
"RegisterInstanceAs",
new CodeTypeReference(OUTPUT_INTERFACE)),
new CodeVariableReferenceExpression(OUTPUT_INTERFACE_FIELD_NAME)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,12 @@ public void SetTestClassInitializeMethod(TestClassGenerationContext generationCo
var fixtureDataType =
CodeDomHelper.CreateNestedTypeReference(generationContext.TestClass, _currentFixtureDataTypeDeclaration.Name);

var useFixtureType = CreateFixtureInterface(fixtureDataType);
var useFixtureType = CreateFixtureInterface(generationContext, fixtureDataType);

CodeDomHelper.SetTypeReferenceAsInterface(useFixtureType);

generationContext.TestClass.BaseTypes.Add(useFixtureType);

// public void SetFixture(T) { } // explicit interface implementation for generic interfaces does not work with codedom

CodeMemberMethod setFixtureMethod = new CodeMemberMethod();
setFixtureMethod.Attributes = MemberAttributes.Public;
setFixtureMethod.Name = "SetFixture";
setFixtureMethod.Parameters.Add(new CodeParameterDeclarationExpression(fixtureDataType, "fixtureData"));
if (ImplmentInterfaceExplicit)
{
setFixtureMethod.ImplementationTypes.Add(useFixtureType);
}
generationContext.TestClass.Members.Add(setFixtureMethod);

// public <_currentFixtureTypeDeclaration>() { <fixtureSetupMethod>(); }
CodeConstructor ctorMethod = new CodeConstructor();
ctorMethod.Attributes = MemberAttributes.Public;
Expand Down Expand Up @@ -165,10 +153,7 @@ public void SetTestInitializeMethod(TestClassGenerationContext generationContext
ctorMethod.Attributes = MemberAttributes.Public;
generationContext.TestClass.Members.Add(ctorMethod);

ctorMethod.Statements.Add(
new CodeMethodInvokeExpression(
new CodeThisReferenceExpression(),
generationContext.TestInitializeMethod.Name));
SetTestConstructor(generationContext, ctorMethod);
}

public void SetTestCleanupMethod(TestClassGenerationContext generationContext)
Expand Down Expand Up @@ -222,6 +207,13 @@ public virtual void SetTestMethodIgnore(TestClassGenerationContext generationCon
}
}

protected virtual void SetTestConstructor(TestClassGenerationContext generationContext, CodeConstructor ctorMethod) {
ctorMethod.Statements.Add(
new CodeMethodInvokeExpression(
new CodeThisReferenceExpression(),
generationContext.TestInitializeMethod.Name));
}

protected void SetProperty(CodeTypeMember codeTypeMember, string name, string value)
{
CodeDomHelper.AddAttribute(codeTypeMember, TRAIT_ATTRIBUTE, name, value);
Expand All @@ -233,9 +225,19 @@ protected void SetDescription(CodeTypeMember codeTypeMember, string description)
SetProperty(codeTypeMember, DESCRIPTION_PROPERTY_NAME, description);
}

protected virtual CodeTypeReference CreateFixtureInterface(CodeTypeReference fixtureDataType)
protected virtual CodeTypeReference CreateFixtureInterface(TestClassGenerationContext generationContext, CodeTypeReference fixtureDataType)
{
return new CodeTypeReference(IUSEFIXTURE_INTERFACE, fixtureDataType);
var useFixtureType = new CodeTypeReference(IUSEFIXTURE_INTERFACE, fixtureDataType);
// public void SetFixture(T) { } // explicit interface implementation for generic interfaces does not work with codedom

CodeMemberMethod setFixtureMethod = new CodeMemberMethod();
setFixtureMethod.Attributes = MemberAttributes.Public;
setFixtureMethod.Name = "SetFixture";
setFixtureMethod.Parameters.Add(new CodeParameterDeclarationExpression(fixtureDataType, "fixtureData"));
setFixtureMethod.ImplementationTypes.Add(useFixtureType);
generationContext.TestClass.Members.Add(setFixtureMethod);

return useFixtureType;
}

public virtual void FinalizeTestClass(TestClassGenerationContext generationContext)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.CodeDom;
using System.Globalization;
using System.IO;
using System.Linq;

using NUnit.Framework;
Expand All @@ -8,12 +10,22 @@
using TechTalk.SpecFlow.Parser;
using TechTalk.SpecFlow.Generator.UnitTestProvider;

using FluentAssertions;

namespace TechTalk.SpecFlow.GeneratorTests
{
[TestFixture]
public class XUnit2TestGeneratorProviderTests
{
private const string SampleFeatureFile = @"
Feature: Sample feature file
Scenario: Simple scenario
Given there is something
When I do something
Then something should happen";


[Test]
public void Should_set_displayname_theory_attribute()
{
Expand Down Expand Up @@ -62,5 +74,84 @@ public void Should_set_displayname_theory_attribute()
Assert.That(primitiveExpression, Is.Not.Null);
Assert.That(primitiveExpression.Value, Is.EqualTo("Foo"));
}

[Test]
public void Should_initialize_testOutputHelper_field_in_constructor()
{
SpecFlowGherkinParser parser = new SpecFlowGherkinParser(new CultureInfo("en-US"));
using (var reader = new StringReader(SampleFeatureFile))
{
SpecFlowDocument document = parser.Parse(reader, null);
Assert.IsNotNull(document);

var provider = new XUnit2TestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp));

var converter = provider.CreateUnitTestConverter();
CodeNamespace code = converter.GenerateUnitTestFixture(document, "TestClassName", "Target.Namespace");

Assert.IsNotNull(code);
var classContructor = code.Class().Members().Single(m => m.Name == ".ctor");
classContructor.Should().NotBeNull();
classContructor.Parameters.Count.Should().Be(2);
classContructor.Parameters[1].Type.BaseType.Should().Be("Xunit.Abstractions.ITestOutputHelper");
classContructor.Parameters[1].Name.Should().Be("testOutputHelper");

var initOutputHelper = classContructor.Statements.OfType<CodeAssignStatement>().First();
initOutputHelper.Should().NotBeNull();
((CodeFieldReferenceExpression)(initOutputHelper.Left)).FieldName.Should().Be("_testOutputHelper");
((CodeVariableReferenceExpression)(initOutputHelper.Right)).VariableName.Should().Be("testOutputHelper");
}
}

[Test]
public void Should_add_testOutputHelper_field_in_class()
{
SpecFlowGherkinParser parser = new SpecFlowGherkinParser(new CultureInfo("en-US"));
using (var reader = new StringReader(SampleFeatureFile))
{
SpecFlowDocument document = parser.Parse(reader, null);
Assert.IsNotNull(document);

var provider = new XUnit2TestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp));

var converter = provider.CreateUnitTestConverter();
CodeNamespace code = converter.GenerateUnitTestFixture(document, "TestClassName", "Target.Namespace");

Assert.IsNotNull(code);
var loggerInstance = code.Class().Members.OfType<CodeMemberField>().First(m => m.Name == @"_testOutputHelper");
loggerInstance.Type.BaseType.Should().Be("Xunit.Abstractions.ITestOutputHelper");
loggerInstance.Attributes.Should().Be(MemberAttributes.Private | MemberAttributes.Final);
}
}

[Test]
public void Should_register_testOutputHelper_on_scenario_setup()
{
SpecFlowGherkinParser parser = new SpecFlowGherkinParser(new CultureInfo("en-US"));
using (var reader = new StringReader(SampleFeatureFile))
{
SpecFlowDocument document = parser.Parse(reader, null);
Assert.IsNotNull(document);

var provider = new XUnit2TestGeneratorProvider(new CodeDomHelper(CodeDomProviderLanguage.CSharp));

var converter = provider.CreateUnitTestConverter();
CodeNamespace code = converter.GenerateUnitTestFixture(document, "TestClassName", "Target.Namespace");

Assert.IsNotNull(code);
var scenarioStartMethod = code.Class().Members().Single(m => m.Name == @"ScenarioSetup");

scenarioStartMethod.Statements.Count.Should().Be(2);
var expression = (scenarioStartMethod.Statements[1] as CodeExpressionStatement).Expression;
var method = (expression as CodeMethodInvokeExpression).Method;
(method.TargetObject as CodePropertyReferenceExpression).PropertyName.Should().Be("ScenarioContainer");
method.MethodName.Should().Be("RegisterInstanceAs");
method.TypeArguments.Should().NotBeNullOrEmpty();
method.TypeArguments[0].BaseType.Should().Be("Xunit.Abstractions.ITestOutputHelper");

((expression as CodeMethodInvokeExpression).Parameters[0] as CodeVariableReferenceExpression).VariableName.Should().Be("_testOutputHelper");
}
}

}
}
21 changes: 10 additions & 11 deletions Tests/TechTalk.SpecFlow.Specs/Drivers/XUnitTestExecutionDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,26 @@ public TestRunSummary Execute()
var xunitConsolePath = Path.Combine(AssemblyFolderHelper.GetTestAssemblyFolder(), @"xunit.runner.console\tools\xunit.console.exe");

var provessHelper = new ProcessHelper();
provessHelper.RunProcess(xunitConsolePath, "\"{0}\" -nunit \"{1}\"",
provessHelper.RunProcess(xunitConsolePath, "\"{0}\" -xml \"{1}\"",
inputProjectDriver.CompiledAssemblyPath, resultFilePath);

File.WriteAllText(logFilePath, provessHelper.ConsoleOutput);
return ProcessNUnitResult(logFilePath, resultFilePath);
return ProcessXUnitResult(logFilePath, resultFilePath);
}

private TestRunSummary ProcessNUnitResult(string logFilePath, string resultFilePath)
private TestRunSummary ProcessXUnitResult(string logFilePath, string resultFilePath)
{
XDocument resultFileXml = XDocument.Load(resultFilePath);

TestRunSummary summary = new TestRunSummary();

summary.Total = resultFileXml.XPathSelectElements("//test-case").Count();
summary.Succeeded = resultFileXml.XPathSelectElements("//test-case[@executed = 'True' and @success='True']").Count();
summary.Total = resultFileXml.XPathSelectElements("//test").Count();
summary.Succeeded = resultFileXml.XPathSelectElements("//test[@result = 'Pass']").Count();
summary.Failed =
resultFileXml.XPathSelectElements("//test-case[@executed = 'True' and @success='False' and failure]").Count();
summary.Pending =
resultFileXml.XPathSelectElements("//test-case[@executed = 'True' and @success='False' and not(failure)]").Count
resultFileXml.XPathSelectElements("//test[@result = 'Fail']").Count();
summary.Ignored =
resultFileXml.XPathSelectElements("//test[@result = 'Skip']").Count
();
summary.Ignored = resultFileXml.XPathSelectElements("//test-case[@executed = 'False']").Count();

testExecutionResult.LastExecutionSummary = summary;
testExecutionResult.ExecutionLog = File.ReadAllText(logFilePath);
Expand All @@ -55,6 +54,6 @@ private TestRunSummary ProcessNUnitResult(string logFilePath, string resultFileP
Console.WriteLine(testExecutionResult.ExecutionLog);

return summary;
}
}
}
}
55 changes: 54 additions & 1 deletion Tests/TechTalk.SpecFlow.Specs/Features/XUnit2Provider.feature
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,57 @@ Scenario: Should be able to specify xUnit provider in the configuration
Then the execution summary should contain
| Total |
| 1 |


Scenario: Should be able to log custom messages
Given there is a SpecFlow project
And the project is configured to use the xUnit provider
And a scenario 'Simple Scenario' as
"""
When I do something
"""
And the following step definition
"""
[When(@"I do something")]
public void WhenIDoSomething()
{
ScenarioContext.Current.ScenarioContainer.Resolve<Xunit.Abstractions.ITestOutputHelper>().WriteLine("hello");
}
"""
When I execute the tests with xUnit
Then the execution summary should contain
| Succeeded |
| 1 |



Scenario: Should be able to log custom messages using context injection
Given there is a SpecFlow project
And the project is configured to use the xUnit provider
And the following binding class
"""
[Binding]
public class StepsWithScenarioContext
{
private ScenarioContext _scenarioContext;
private Xunit.Abstractions.ITestOutputHelper _output;
public StepsWithScenarioContext(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
_output = _scenarioContext.ScenarioContainer.Resolve<Xunit.Abstractions.ITestOutputHelper>();
}
[When(@"I do something")]
public void WhenIDoSomething()
{
_output.WriteLine("something");
}
}
"""
And a scenario 'Simple Scenario' as
"""
When I do something
"""
When I execute the tests with xUnit
Then the execution summary should contain
| Succeeded |
| 1 |

0 comments on commit 8bcab7f

Please sign in to comment.