xunit / devices.xunit

xUnit.net Runners for Devices
Other
73 stars 36 forks source link

Improves the ability to customize the report output #69

Closed dotMorten closed 6 years ago

dotMorten commented 6 years ago

With this change, I was much more able to control the test output, logging directly to a sql server database, or customize the output to write out a TRX report instead.

Here's an example of a TRX channel:

public class TrxResultChannel : Xunit.Runners.IResultChannel
{
    const string xmlNamespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
    private string path;
    private XmlDocument _doc;
    private int testCount;
    private int testFailed;
    private int testSucceeded;

    private XmlElement _rootNode;
    private XmlElement _resultsNode;
    private XmlElement _testDefinitions;
    private XmlElement _header;

    public TrxResultChannel(string path)
    {
        this.path = path;
    }

    public Task<bool> OpenChannel(string message = null)
    {
        testCount = testFailed = testSucceeded = 0;
        _doc = new XmlDocument();
        _rootNode = _doc.CreateElement("TestRun", xmlNamespace);
        _rootNode.SetAttribute("id", Guid.NewGuid().ToString());
        _rootNode.SetAttribute("name", "");
        _rootNode.SetAttribute("runUser", "");
        _doc.AppendChild(_rootNode);
        _header = _doc.CreateElement("Times", xmlNamespace);
        _header.SetAttribute("finish", DateTime.Now.ToString("O"));
        _header.SetAttribute("start", DateTime.Now.ToString("O"));
        _header.SetAttribute("creation", DateTime.Now.ToString("O"));
        _rootNode.AppendChild(_header);
        _resultsNode = _doc.CreateElement("Results", xmlNamespace);
        _rootNode.AppendChild(_resultsNode);
        _testDefinitions = _doc.CreateElement("TestDefinitions", xmlNamespace);
        _rootNode.AppendChild(_testDefinitions);
        return Task.FromResult(true);
    }

    public void RecordResult(TestResultViewModel result)
    {
        string id = Guid.NewGuid().ToString();
        var resultNode = _doc.CreateElement("UnitTestResult", xmlNamespace);
        resultNode.SetAttribute("outcome", ToTrxStatus(result.TestCase.Result));
        int idx = result.TestCase.DisplayName.LastIndexOf('.');
        string testName = result.TestCase.TestCase.TestMethod.Method.Name;
        string className = result.TestCase.TestCase.TestMethod.TestClass.Class.Name;
        resultNode.SetAttribute("testName", testName);
        resultNode.SetAttribute("testId", id);
        resultNode.SetAttribute("duration", result.Duration.ToString());

        if (result.TestCase.Result == TestState.Failed)
        {
            testFailed++;
            var output = _doc.CreateElement("Output", xmlNamespace);
            var errorInfo = _doc.CreateElement("ErrorInfo", xmlNamespace);
            var message = _doc.CreateElement("Message", xmlNamespace);
            message.InnerText = result.ErrorMessage;
            var stackTrace = _doc.CreateElement("StackTrace", xmlNamespace);
            stackTrace.InnerText = result.ErrorStackTrace;
            output.AppendChild(errorInfo);
            errorInfo.AppendChild(message);
            errorInfo.AppendChild(stackTrace);
            resultNode.AppendChild(output);
        }
        else
        {
            testSucceeded++;
        }
        testCount++;

        _resultsNode.AppendChild(resultNode);

        var testNode = _doc.CreateElement("UnitTest", xmlNamespace);
        testNode.SetAttribute("name", testName);
        testNode.SetAttribute("id", id);
        var testMethodName = _doc.CreateElement("TestMethod", xmlNamespace);
        testMethodName.SetAttribute("name", testName);
        testMethodName.SetAttribute("className", className);
        testNode.AppendChild(testMethodName);
        XmlElement properties = null;
        List<string> categories = null;
        foreach (var prop in result.TestCase.TestCase.Traits)
        {
            if(prop.Key == "Category")
            {
                categories = prop.Value;
                continue;
            }
            foreach (var v in prop.Value)
            {
                if (properties == null)
                {
                    properties = _doc.CreateElement("Properties", xmlNamespace);
                    testNode.AppendChild(properties);
                }

                var property = _doc.CreateElement("Property", xmlNamespace);
                var key = _doc.CreateElement("Key", xmlNamespace);
                key.InnerText = prop.Key;
                property.AppendChild(key);
                var value = _doc.CreateElement("Value", xmlNamespace);
                value.InnerText = v;
                property.AppendChild(value);
                properties.AppendChild(property);
            }
        }

        if (categories != null && categories.Any())
        {
            var testCategory = _doc.CreateElement("TestCategory", xmlNamespace);
            foreach (var category in categories)
            {
                var item = _doc.CreateElement("TestCategoryItem", xmlNamespace);
                item.SetAttribute("TestCategory", category);
                testCategory.AppendChild(item);
            }
            testNode.AppendChild(testCategory);
        }

        _testDefinitions.AppendChild(testNode);
    }

    public Task CloseChannel()
    {
        _header.SetAttribute("finish", DateTime.Now.ToString("O"));
        var resultSummary = _doc.CreateElement("ResultSummary", xmlNamespace);
        resultSummary.SetAttribute("outcome", "Completed");
        var counters = _doc.CreateElement("Counters", xmlNamespace);
        counters.SetAttribute("passed", testSucceeded.ToString());
        counters.SetAttribute("failed", testFailed.ToString());
        counters.SetAttribute("total", testCount.ToString()); ;
        resultSummary.AppendChild(counters);
        _rootNode.AppendChild(resultSummary);
        _doc.Save(path);
        return Task.CompletedTask;
    }

    private static string ToTrxStatus(TestState result)
    {
        switch(result)
        {
            case TestState.Failed: return "Failed";
            case TestState.Passed: return "Passed";
            case TestState.NotRun: return "NotRunnable";
            case TestState.Skipped: return "NotExecuted";
            default: return "";
        }
    }
}

Note: this isn't a breaking change, but it is making the old TextWriter property obsolete.

clairernovotny commented 6 years ago

This looks good -- can you please include the TrxResultChannel in the runner so others can use it too?

dotMorten commented 6 years ago

@onovotny The TRX reporter needs some more work / testing. Can we do that as a separate PR?

clairernovotny commented 6 years ago

Thanks!

As part of that PR, can you make the existing ResultsListener public and rename it to be TextWriterResultChannel? Feel free to add a common base if there's any useful shared functionality.

dotMorten commented 6 years ago

Sorry just saw this comment now :-) That's for doing it for me