From 693652333bd6f4395ee24b35eda54f13fa5404b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 19:15:27 +0000
Subject: [PATCH 1/6] Initial plan
From 808b7da346fa065b0517665d010acefa8e7688ce Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 19:20:32 +0000
Subject: [PATCH 2/6] Add JUnitSerializer with comprehensive tests
Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
---
.../IO/JUnitSerializer.cs | 142 ++++++
.../IO/JUnitSerializerTests.cs | 438 ++++++++++++++++++
2 files changed, 580 insertions(+)
create mode 100644 src/DemaConsulting.TestResults/IO/JUnitSerializer.cs
create mode 100644 test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
diff --git a/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs b/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs
new file mode 100644
index 0000000..4b7b483
--- /dev/null
+++ b/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs
@@ -0,0 +1,142 @@
+// Copyright(c) 2025 DEMA Consulting
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System.Globalization;
+using System.Text;
+using System.Xml.Linq;
+
+namespace DemaConsulting.TestResults.IO;
+
+///
+/// JUnit Serializer class
+///
+public static class JUnitSerializer
+{
+ ///
+ /// Serializes the TestResults object to a JUnit XML file
+ ///
+ /// Test Results
+ /// JUnit XML file contents
+ public static string Serialize(TestResults results)
+ {
+ // Group test results by class name for test suites
+ var testSuites = results.Results
+ .GroupBy(r => r.ClassName)
+ .OrderBy(g => g.Key);
+
+ // Construct the root element
+ var root = new XElement("testsuites",
+ new XAttribute("name", results.Name));
+
+ // Add test suites for each class
+ foreach (var suiteGroup in testSuites)
+ {
+ var className = string.IsNullOrEmpty(suiteGroup.Key) ? "DefaultSuite" : suiteGroup.Key;
+ var suiteTests = suiteGroup.ToList();
+
+ var testSuite = new XElement("testsuite",
+ new XAttribute("name", className),
+ new XAttribute("tests", suiteTests.Count),
+ new XAttribute("failures", suiteTests.Count(t => t.Outcome == TestOutcome.Failed)),
+ new XAttribute("errors", suiteTests.Count(t => t.Outcome == TestOutcome.Error || t.Outcome == TestOutcome.Timeout || t.Outcome == TestOutcome.Aborted)),
+ new XAttribute("skipped", suiteTests.Count(t => !t.Outcome.IsExecuted())),
+ new XAttribute("time", suiteTests.Sum(t => t.Duration.TotalSeconds).ToString("F3", CultureInfo.InvariantCulture)));
+
+ // Add test cases
+ foreach (var test in suiteTests)
+ {
+ var testCase = new XElement("testcase",
+ new XAttribute("name", test.Name),
+ new XAttribute("classname", string.IsNullOrEmpty(test.ClassName) ? "DefaultSuite" : test.ClassName),
+ new XAttribute("time", test.Duration.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)));
+
+ // Add failure or error information
+ if (test.Outcome == TestOutcome.Failed)
+ {
+ var failure = new XElement("failure");
+ if (!string.IsNullOrEmpty(test.ErrorMessage))
+ {
+ failure.Add(new XAttribute("message", test.ErrorMessage));
+ }
+ if (!string.IsNullOrEmpty(test.ErrorStackTrace))
+ {
+ failure.Add(new XCData(test.ErrorStackTrace));
+ }
+ testCase.Add(failure);
+ }
+ else if (test.Outcome == TestOutcome.Error || test.Outcome == TestOutcome.Timeout || test.Outcome == TestOutcome.Aborted)
+ {
+ var error = new XElement("error");
+ if (!string.IsNullOrEmpty(test.ErrorMessage))
+ {
+ error.Add(new XAttribute("message", test.ErrorMessage));
+ }
+ if (!string.IsNullOrEmpty(test.ErrorStackTrace))
+ {
+ error.Add(new XCData(test.ErrorStackTrace));
+ }
+ testCase.Add(error);
+ }
+ else if (!test.Outcome.IsExecuted())
+ {
+ var skipped = new XElement("skipped");
+ if (!string.IsNullOrEmpty(test.ErrorMessage))
+ {
+ skipped.Add(new XAttribute("message", test.ErrorMessage));
+ }
+ testCase.Add(skipped);
+ }
+
+ // Add system output
+ if (!string.IsNullOrEmpty(test.SystemOutput))
+ {
+ testCase.Add(new XElement("system-out", new XCData(test.SystemOutput)));
+ }
+
+ // Add system error
+ if (!string.IsNullOrEmpty(test.SystemError))
+ {
+ testCase.Add(new XElement("system-err", new XCData(test.SystemError)));
+ }
+
+ testSuite.Add(testCase);
+ }
+
+ root.Add(testSuite);
+ }
+
+ // Write the XML text
+ var doc = new XDocument(root);
+ var writer = new Utf8StringWriter();
+ doc.Save(writer);
+ return writer.ToString();
+ }
+
+ ///
+ /// String writer that uses UTF-8 encoding
+ ///
+ private sealed class Utf8StringWriter : StringWriter
+ {
+ ///
+ /// Gets the UTF-8 encoding
+ ///
+ public override Encoding Encoding => Encoding.UTF8;
+ }
+}
diff --git a/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs b/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
new file mode 100644
index 0000000..3423f49
--- /dev/null
+++ b/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
@@ -0,0 +1,438 @@
+// Copyright(c) 2025 DEMA Consulting
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System.Xml.Linq;
+using DemaConsulting.TestResults.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace DemaConsulting.TestResults.Tests.IO;
+
+///
+/// Tests for JUnitSerializer class
+///
+[TestClass]
+public sealed class JUnitSerializerTests
+{
+ ///
+ /// Test for basic serialization with passed test
+ ///
+ [TestMethod]
+ public void TestSerializeBasic()
+ {
+ // Construct a basic test results object
+ var results = new TestResults
+ {
+ Name = "BasicTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test1",
+ ClassName = "MyTestClass",
+ Duration = TimeSpan.FromSeconds(1.5),
+ Outcome = TestOutcome.Passed
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var root = doc.Root;
+ Assert.IsNotNull(root);
+ Assert.AreEqual("testsuites", root.Name.LocalName);
+ Assert.AreEqual("BasicTests", root.Attribute("name")?.Value);
+
+ // Verify test suite
+ var testSuite = root.Element("testsuite");
+ Assert.IsNotNull(testSuite);
+ Assert.AreEqual("MyTestClass", testSuite.Attribute("name")?.Value);
+ Assert.AreEqual("1", testSuite.Attribute("tests")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("failures")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("errors")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("skipped")?.Value);
+ Assert.AreEqual("1.500", testSuite.Attribute("time")?.Value);
+
+ // Verify test case
+ var testCase = testSuite.Element("testcase");
+ Assert.IsNotNull(testCase);
+ Assert.AreEqual("Test1", testCase.Attribute("name")?.Value);
+ Assert.AreEqual("MyTestClass", testCase.Attribute("classname")?.Value);
+ Assert.AreEqual("1.500", testCase.Attribute("time")?.Value);
+
+ // Verify no failure/error/skipped elements for passed test
+ Assert.IsNull(testCase.Element("failure"));
+ Assert.IsNull(testCase.Element("error"));
+ Assert.IsNull(testCase.Element("skipped"));
+ }
+
+ ///
+ /// Test for serialization with failed test
+ ///
+ [TestMethod]
+ public void TestSerializeWithFailure()
+ {
+ // Construct test results with a failed test
+ var results = new TestResults
+ {
+ Name = "FailureTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test2",
+ ClassName = "MyTestClass",
+ Duration = TimeSpan.FromSeconds(0.5),
+ Outcome = TestOutcome.Failed,
+ ErrorMessage = "Expected value to be 42 but was 0",
+ ErrorStackTrace = "at MyTestClass.Test2() in Test.cs:line 15"
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var testSuite = doc.Root?.Element("testsuite");
+ Assert.IsNotNull(testSuite);
+ Assert.AreEqual("1", testSuite.Attribute("tests")?.Value);
+ Assert.AreEqual("1", testSuite.Attribute("failures")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("errors")?.Value);
+
+ // Verify test case with failure
+ var testCase = testSuite.Element("testcase");
+ Assert.IsNotNull(testCase);
+ Assert.AreEqual("Test2", testCase.Attribute("name")?.Value);
+
+ // Verify failure element
+ var failure = testCase.Element("failure");
+ Assert.IsNotNull(failure);
+ Assert.AreEqual("Expected value to be 42 but was 0", failure.Attribute("message")?.Value);
+ Assert.Contains("at MyTestClass.Test2() in Test.cs:line 15", failure.Value);
+ }
+
+ ///
+ /// Test for serialization with error test
+ ///
+ [TestMethod]
+ public void TestSerializeWithError()
+ {
+ // Construct test results with an error test
+ var results = new TestResults
+ {
+ Name = "ErrorTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test3",
+ ClassName = "MyTestClass",
+ Duration = TimeSpan.FromSeconds(0.1),
+ Outcome = TestOutcome.Error,
+ ErrorMessage = "Unexpected exception occurred",
+ ErrorStackTrace = "at MyTestClass.Test3() in Test.cs:line 20"
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var testSuite = doc.Root?.Element("testsuite");
+ Assert.IsNotNull(testSuite);
+ Assert.AreEqual("1", testSuite.Attribute("tests")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("failures")?.Value);
+ Assert.AreEqual("1", testSuite.Attribute("errors")?.Value);
+
+ // Verify test case with error
+ var testCase = testSuite.Element("testcase");
+ Assert.IsNotNull(testCase);
+ var error = testCase.Element("error");
+ Assert.IsNotNull(error);
+ Assert.AreEqual("Unexpected exception occurred", error.Attribute("message")?.Value);
+ Assert.Contains("at MyTestClass.Test3() in Test.cs:line 20", error.Value);
+ }
+
+ ///
+ /// Test for serialization with skipped test
+ ///
+ [TestMethod]
+ public void TestSerializeWithSkipped()
+ {
+ // Construct test results with a skipped test
+ var results = new TestResults
+ {
+ Name = "SkippedTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test4",
+ ClassName = "MyTestClass",
+ Duration = TimeSpan.Zero,
+ Outcome = TestOutcome.NotExecuted,
+ ErrorMessage = "Test was skipped"
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var testSuite = doc.Root?.Element("testsuite");
+ Assert.IsNotNull(testSuite);
+ Assert.AreEqual("1", testSuite.Attribute("tests")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("failures")?.Value);
+ Assert.AreEqual("0", testSuite.Attribute("errors")?.Value);
+ Assert.AreEqual("1", testSuite.Attribute("skipped")?.Value);
+
+ // Verify test case with skipped element
+ var testCase = testSuite.Element("testcase");
+ Assert.IsNotNull(testCase);
+ var skipped = testCase.Element("skipped");
+ Assert.IsNotNull(skipped);
+ Assert.AreEqual("Test was skipped", skipped.Attribute("message")?.Value);
+ }
+
+ ///
+ /// Test for serialization with system output
+ ///
+ [TestMethod]
+ public void TestSerializeWithSystemOutput()
+ {
+ // Construct test results with system output
+ var results = new TestResults
+ {
+ Name = "OutputTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test5",
+ ClassName = "MyTestClass",
+ Duration = TimeSpan.FromSeconds(1.0),
+ Outcome = TestOutcome.Passed,
+ SystemOutput = "Standard output message",
+ SystemError = "Standard error message"
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var testCase = doc.Root?.Element("testsuite")?.Element("testcase");
+ Assert.IsNotNull(testCase);
+
+ // Verify system-out element
+ var systemOut = testCase.Element("system-out");
+ Assert.IsNotNull(systemOut);
+ Assert.AreEqual("Standard output message", systemOut.Value);
+
+ // Verify system-err element
+ var systemErr = testCase.Element("system-err");
+ Assert.IsNotNull(systemErr);
+ Assert.AreEqual("Standard error message", systemErr.Value);
+ }
+
+ ///
+ /// Test for serialization with multiple test results
+ ///
+ [TestMethod]
+ public void TestSerializeMultipleTests()
+ {
+ // Construct test results with multiple tests
+ var results = new TestResults
+ {
+ Name = "MultipleTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test1",
+ ClassName = "Class1",
+ Duration = TimeSpan.FromSeconds(1.0),
+ Outcome = TestOutcome.Passed
+ },
+ new TestResult
+ {
+ Name = "Test2",
+ ClassName = "Class1",
+ Duration = TimeSpan.FromSeconds(0.5),
+ Outcome = TestOutcome.Failed,
+ ErrorMessage = "Test failed"
+ },
+ new TestResult
+ {
+ Name = "Test3",
+ ClassName = "Class2",
+ Duration = TimeSpan.FromSeconds(2.0),
+ Outcome = TestOutcome.Passed
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var root = doc.Root;
+ Assert.IsNotNull(root);
+
+ // Verify two test suites (one for each class)
+ var testSuites = root.Elements("testsuite").ToList();
+ Assert.HasCount(2, testSuites);
+
+ // Verify first test suite (Class1)
+ var suite1 = testSuites[0];
+ Assert.AreEqual("Class1", suite1.Attribute("name")?.Value);
+ Assert.AreEqual("2", suite1.Attribute("tests")?.Value);
+ Assert.AreEqual("1", suite1.Attribute("failures")?.Value);
+ Assert.AreEqual("1.500", suite1.Attribute("time")?.Value);
+
+ // Verify second test suite (Class2)
+ var suite2 = testSuites[1];
+ Assert.AreEqual("Class2", suite2.Attribute("name")?.Value);
+ Assert.AreEqual("1", suite2.Attribute("tests")?.Value);
+ Assert.AreEqual("0", suite2.Attribute("failures")?.Value);
+ Assert.AreEqual("2.000", suite2.Attribute("time")?.Value);
+ }
+
+ ///
+ /// Test for serialization with empty class name
+ ///
+ [TestMethod]
+ public void TestSerializeEmptyClassName()
+ {
+ // Construct test results with empty class name
+ var results = new TestResults
+ {
+ Name = "EmptyClassTests",
+ Results =
+ [
+ new TestResult
+ {
+ Name = "Test1",
+ ClassName = string.Empty,
+ Duration = TimeSpan.FromSeconds(1.0),
+ Outcome = TestOutcome.Passed
+ }
+ ]
+ };
+
+ // Serialize the test results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the XML structure
+ var doc = XDocument.Parse(xml);
+ var testSuite = doc.Root?.Element("testsuite");
+ Assert.IsNotNull(testSuite);
+ Assert.AreEqual("DefaultSuite", testSuite.Attribute("name")?.Value);
+
+ var testCase = testSuite.Element("testcase");
+ Assert.IsNotNull(testCase);
+ Assert.AreEqual("DefaultSuite", testCase.Attribute("classname")?.Value);
+ }
+
+ ///
+ /// Test for serialization matching the usage example from the issue
+ ///
+ [TestMethod]
+ public void TestSerializeUsageExample()
+ {
+ // Create a TestResults instance matching the usage example
+ var results = new TestResults { Name = "SomeTests" };
+
+ // Add some results
+ results.Results.Add(
+ new TestResult
+ {
+ Name = "Test1",
+ ClassName = "SomeTestClass",
+ CodeBase = "MyTestAssembly",
+ Outcome = TestOutcome.Passed,
+ Duration = TimeSpan.FromSeconds(1.5),
+ StartTime = DateTime.UtcNow,
+ });
+
+ results.Results.Add(
+ new TestResult
+ {
+ Name = "Test2",
+ ClassName = "SomeTestClass",
+ CodeBase = "MyTestAssembly",
+ Outcome = TestOutcome.Failed,
+ ErrorMessage = "Expected value to be 42 but was 0",
+ ErrorStackTrace = "at SomeTestClass.Test2() in Test.cs:line 15"
+ });
+
+ // Serialize the results
+ var xml = JUnitSerializer.Serialize(results);
+ Assert.IsNotNull(xml);
+
+ // Parse and verify the structure matches expected JUnit format
+ var doc = XDocument.Parse(xml);
+ var root = doc.Root;
+ Assert.IsNotNull(root);
+ Assert.AreEqual("testsuites", root.Name.LocalName);
+ Assert.AreEqual("SomeTests", root.Attribute("name")?.Value);
+
+ // Verify test suite
+ var testSuite = root.Element("testsuite");
+ Assert.IsNotNull(testSuite);
+ Assert.AreEqual("SomeTestClass", testSuite.Attribute("name")?.Value);
+ Assert.AreEqual("2", testSuite.Attribute("tests")?.Value);
+ Assert.AreEqual("1", testSuite.Attribute("failures")?.Value);
+
+ // Verify both test cases are present
+ var testCases = testSuite.Elements("testcase").ToList();
+ Assert.HasCount(2, testCases);
+
+ // Verify passed test
+ var passedTest = testCases.FirstOrDefault(tc => tc.Attribute("name")?.Value == "Test1");
+ Assert.IsNotNull(passedTest);
+ Assert.IsNull(passedTest.Element("failure"));
+
+ // Verify failed test
+ var failedTest = testCases.FirstOrDefault(tc => tc.Attribute("name")?.Value == "Test2");
+ Assert.IsNotNull(failedTest);
+ var failure = failedTest.Element("failure");
+ Assert.IsNotNull(failure);
+ Assert.AreEqual("Expected value to be 42 but was 0", failure.Attribute("message")?.Value);
+ }
+}
From 4f236054383f8e5ddef37903b55a5581f47f6ee3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 19:40:08 +0000
Subject: [PATCH 3/6] Update documentation to reflect both TRX and JUnit format
support
Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
---
AGENTS.md | 2 +-
ARCHITECTURE.md | 27 +++++++++++++++++---------
README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++-----
SECURITY.md | 4 ++--
4 files changed, 66 insertions(+), 17 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 193499c..a07e829 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,7 +12,7 @@ TestResults is a C# library for creating test result files. The library is desig
- **Type-Safe**: Strongly-typed C# objects
- **Cross-Platform**: Supports .NET 8, 9, and 10
-The library currently focuses on TRX format, with potential for additional formats in the future.
+The library supports both TRX and JUnit XML formats, with potential for additional formats in the future.
## Technologies and Dependencies
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 692f234..ec929dd 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -3,9 +3,9 @@
## Overview
The TestResults library is a lightweight C# library designed to programmatically create test result files.
-The library currently focuses on the TRX (Test Results) format, with potential for additional formats in the future.
+The library supports both TRX (Test Results) and JUnit XML formats.
TRX files are XML-based test result files commonly used by Visual Studio, Azure DevOps, and other Microsoft testing
-tools to store and visualize test execution results.
+tools, while JUnit XML is widely used across various CI/CD systems and testing frameworks.
## Design Philosophy
@@ -67,13 +67,22 @@ The `TrxSerializer` class is responsible for converting the domain model into TR
- Produces TRX files compatible with Visual Studio and Azure DevOps
- Handles proper formatting and schema compliance
+#### `JUnitSerializer`
+
+The `JUnitSerializer` class is responsible for converting the domain model into JUnit XML format:
+
+- Uses .NET's built-in XML serialization capabilities
+- Produces JUnit XML files compatible with various CI/CD systems and testing tools
+- Groups test results by class name into test suites
+- Maps test outcomes to JUnit semantics (failure, error, skipped)
+
## Data Flow
```text
1. User creates TestResults instance
2. User adds TestResult objects to the Results collection
-3. User calls TrxSerializer.Serialize() to convert to TRX XML format
-4. User saves the XML string to a .trx file
+3. User calls TrxSerializer.Serialize() or JUnitSerializer.Serialize() to convert to desired format
+4. User saves the XML string to a .trx or .xml file
```
## Design Patterns
@@ -95,8 +104,8 @@ The library source code is organized in the `/src/DemaConsulting.TestResults/` d
The library is designed to be extended in several ways:
1. **Custom Test Outcomes**: While the standard outcomes cover most scenarios, custom outcomes could be added
-2. **Additional Metadata**: The model could be extended to support additional TRX metadata fields
-3. **Alternative Serializers**: Additional serializers could be added for other test result formats
+2. **Additional Metadata**: The model could be extended to support additional metadata fields
+3. **Alternative Serializers**: Additional serializers could be added for other test result formats (NUnit, xUnit, etc.)
## Quality Attributes
@@ -128,10 +137,10 @@ The library is designed to be extended in several ways:
Potential enhancements that could be considered:
-1. **Deserialization**: Add support for reading existing TRX files back into the object model
-2. **Additional Formats**: Support for other test result formats (JUnit XML, NUnit XML, etc.)
+1. **Deserialization**: Add support for reading existing TRX and JUnit XML files back into the object model
+2. **Additional Formats**: Support for other test result formats (NUnit XML, xUnit XML, etc.)
3. **Streaming**: Support for streaming large test result sets to avoid memory issues
-4. **Validation**: Add schema validation to ensure generated TRX files are well-formed
+4. **Validation**: Add schema validation to ensure generated files are well-formed
## Dependencies
diff --git a/README.md b/README.md
index 4ee7cc1..bd5b33f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
[](https://sonarcloud.io/summary/new_code?id=demaconsulting_TestResults)
[](https://sonarcloud.io/summary/new_code?id=demaconsulting_TestResults)
-A lightweight C# library for programmatically creating TRX (Test Results) files.
+A lightweight C# library for programmatically creating test result files in TRX and JUnit formats.
## Features
@@ -18,6 +18,7 @@ A lightweight C# library for programmatically creating TRX (Test Results) files.
- 🔄 **Multi-Target** - Supports .NET 8, 9, and 10
- 📦 **NuGet Ready** - Easy integration via NuGet package
- ✅ **Compatible** - Generates TRX files compatible with Visual Studio, Azure DevOps, and other Microsoft testing tools
+- 📊 **Multiple Formats** - Supports both TRX and JUnit XML formats
## Installation
@@ -35,6 +36,8 @@ Install-Package DemaConsulting.TestResults
## Quick Start
+### Creating TRX Files
+
The following code-snippet shows how to create a TRX test-results file:
```csharp
@@ -68,12 +71,49 @@ results.Results.Add(
ErrorStackTrace = "at SomeTestClass.Test2() in Test.cs:line 15"
});
-// Save the results to file
+// Save the results to TRX file
File.WriteAllText(
"results.trx",
TrxSerializer.Serialize(results));
```
+### Creating JUnit XML Files
+
+The following code-snippet shows how to create a JUnit XML test-results file:
+
+```csharp
+using DemaConsulting.TestResults;
+using DemaConsulting.TestResults.IO;
+
+// Create a TestResults instance
+var results = new TestResults { Name = "SomeTests" };
+
+// Add some results
+results.Results.Add(
+ new TestResult
+ {
+ Name = "Test1",
+ ClassName = "SomeTestClass",
+ Outcome = TestOutcome.Passed,
+ Duration = TimeSpan.FromSeconds(1.5)
+ });
+
+results.Results.Add(
+ new TestResult
+ {
+ Name = "Test2",
+ ClassName = "SomeTestClass",
+ Outcome = TestOutcome.Failed,
+ ErrorMessage = "Expected value to be 42 but was 0",
+ ErrorStackTrace = "at SomeTestClass.Test2() in Test.cs:line 15"
+ });
+
+// Save the results to JUnit XML file
+File.WriteAllText(
+ "results.xml",
+ JUnitSerializer.Serialize(results));
+```
+
## Advanced Usage
### Capturing Standard Output
@@ -120,11 +160,11 @@ The library supports the following test outcomes:
This library is useful when you need to:
-- Generate TRX files from custom test runners
-- Convert test results from other formats to TRX
+- Generate TRX or JUnit XML files from custom test runners
+- Convert test results between formats (TRX ↔ JUnit)
- Create test reports programmatically
- Aggregate test results from multiple sources
-- Build custom testing tools that integrate with Visual Studio or Azure DevOps
+- Build custom testing tools that integrate with Visual Studio, Azure DevOps, or CI/CD systems that consume JUnit XML
## Documentation
diff --git a/SECURITY.md b/SECURITY.md
index 7b772e8..f1605ad 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -103,8 +103,8 @@ Historical security advisories can be found in the
When using the TestResults library:
1. **Keep Updated**: Always use the latest version of the library
-2. **Validate Input**: If accepting TRX file paths from users, validate and sanitize them
-3. **File Permissions**: When writing TRX files, use appropriate file permissions
+2. **Validate Input**: If accepting test result file paths from users, validate and sanitize them
+3. **File Permissions**: When writing test result files (TRX or JUnit XML), use appropriate file permissions
4. **Error Handling**: Implement proper error handling when using the library
5. **Code Review**: Review any code that uses this library as part of your security practices
From 4e94fc4b0075d49159e4e9a3e4c26c9a96b07a47 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 19:50:18 +0000
Subject: [PATCH 4/6] Add JUnitSerializer.Deserialize method with comprehensive
tests
Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
---
.../IO/JUnitSerializer.cs | 96 ++++++
.../IO/JUnitSerializerTests.cs | 273 ++++++++++++++++++
2 files changed, 369 insertions(+)
diff --git a/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs b/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs
index 4b7b483..2ec97b4 100644
--- a/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs
+++ b/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs
@@ -129,6 +129,102 @@ public static string Serialize(TestResults results)
return writer.ToString();
}
+ ///
+ /// Deserializes a JUnit XML file to a TestResults object
+ ///
+ /// JUnit XML File Contents
+ /// Test Results
+ public static TestResults Deserialize(string junitContents)
+ {
+ // Parse the document
+ var doc = XDocument.Parse(junitContents);
+
+ // Construct the results
+ var results = new TestResults();
+
+ // Get the root element (testsuites)
+ var rootElement = doc.Root ??
+ throw new InvalidOperationException("Invalid JUnit XML file");
+
+ // Get the test suite name (from testsuites or first testsuite)
+ results.Name = rootElement.Attribute("name")?.Value ?? string.Empty;
+
+ // Handle both testsuites (with nested testsuite) and single testsuite root
+ var testSuiteElements = rootElement.Name.LocalName == "testsuites"
+ ? rootElement.Elements("testsuite")
+ : new[] { rootElement };
+
+ // Process each test suite
+ foreach (var testSuiteElement in testSuiteElements)
+ {
+ // Get test cases
+ var testCaseElements = testSuiteElement.Elements("testcase");
+
+ foreach (var testCaseElement in testCaseElements)
+ {
+ // Parse test case attributes
+ var name = testCaseElement.Attribute("name")?.Value ?? string.Empty;
+ var className = testCaseElement.Attribute("classname")?.Value ?? string.Empty;
+ var timeStr = testCaseElement.Attribute("time")?.Value ?? "0";
+ var duration = double.TryParse(timeStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var timeValue)
+ ? TimeSpan.FromSeconds(timeValue)
+ : TimeSpan.Zero;
+
+ // Determine outcome based on child elements
+ var failureElement = testCaseElement.Element("failure");
+ var errorElement = testCaseElement.Element("error");
+ var skippedElement = testCaseElement.Element("skipped");
+
+ TestOutcome outcome;
+ string errorMessage = string.Empty;
+ string errorStackTrace = string.Empty;
+
+ if (failureElement != null)
+ {
+ outcome = TestOutcome.Failed;
+ errorMessage = failureElement.Attribute("message")?.Value ?? string.Empty;
+ errorStackTrace = failureElement.Value;
+ }
+ else if (errorElement != null)
+ {
+ outcome = TestOutcome.Error;
+ errorMessage = errorElement.Attribute("message")?.Value ?? string.Empty;
+ errorStackTrace = errorElement.Value;
+ }
+ else if (skippedElement != null)
+ {
+ outcome = TestOutcome.NotExecuted;
+ errorMessage = skippedElement.Attribute("message")?.Value ?? string.Empty;
+ }
+ else
+ {
+ outcome = TestOutcome.Passed;
+ }
+
+ // Get system output and error
+ var systemOutput = testCaseElement.Element("system-out")?.Value ?? string.Empty;
+ var systemError = testCaseElement.Element("system-err")?.Value ?? string.Empty;
+
+ // Create test result
+ var testResult = new TestResult
+ {
+ Name = name,
+ ClassName = className == "DefaultSuite" ? string.Empty : className,
+ Duration = duration,
+ Outcome = outcome,
+ ErrorMessage = errorMessage,
+ ErrorStackTrace = errorStackTrace,
+ SystemOutput = systemOutput,
+ SystemError = systemError
+ };
+
+ results.Results.Add(testResult);
+ }
+ }
+
+ return results;
+ }
+
///
/// String writer that uses UTF-8 encoding
///
diff --git a/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs b/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
index 3423f49..0914045 100644
--- a/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
+++ b/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
@@ -435,4 +435,277 @@ public void TestSerializeUsageExample()
Assert.IsNotNull(failure);
Assert.AreEqual("Expected value to be 42 but was 0", failure.Attribute("message")?.Value);
}
+
+ ///
+ /// Test for basic deserialization
+ ///
+ [TestMethod]
+ public void TestDeserializeBasic()
+ {
+ // Deserialize the test results object
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert results information
+ Assert.AreEqual("BasicTests", results.Name);
+ Assert.HasCount(1, results.Results);
+
+ // Assert test result information
+ var result = results.Results[0];
+ Assert.AreEqual("Test1", result.Name);
+ Assert.AreEqual("MyTestClass", result.ClassName);
+ Assert.AreEqual(1.5, result.Duration.TotalSeconds);
+ Assert.AreEqual(TestOutcome.Passed, result.Outcome);
+ }
+
+ ///
+ /// Test for deserialization with failure
+ ///
+ [TestMethod]
+ public void TestDeserializeWithFailure()
+ {
+ // Deserialize the test results object with a failed test
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert results information
+ Assert.AreEqual("FailureTests", results.Name);
+ Assert.HasCount(1, results.Results);
+
+ // Assert test result information
+ var result = results.Results[0];
+ Assert.AreEqual("Test2", result.Name);
+ Assert.AreEqual("MyTestClass", result.ClassName);
+ Assert.AreEqual(TestOutcome.Failed, result.Outcome);
+ Assert.AreEqual("Expected value to be 42 but was 0", result.ErrorMessage);
+ Assert.Contains("at MyTestClass.Test2() in Test.cs:line 15", result.ErrorStackTrace);
+ }
+
+ ///
+ /// Test for deserialization with error
+ ///
+ [TestMethod]
+ public void TestDeserializeWithError()
+ {
+ // Deserialize the test results object with an error test
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert test result information
+ var result = results.Results[0];
+ Assert.AreEqual("Test3", result.Name);
+ Assert.AreEqual(TestOutcome.Error, result.Outcome);
+ Assert.AreEqual("Unexpected exception occurred", result.ErrorMessage);
+ Assert.Contains("at MyTestClass.Test3() in Test.cs:line 20", result.ErrorStackTrace);
+ }
+
+ ///
+ /// Test for deserialization with skipped test
+ ///
+ [TestMethod]
+ public void TestDeserializeWithSkipped()
+ {
+ // Deserialize the test results object with a skipped test
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert test result information
+ var result = results.Results[0];
+ Assert.AreEqual("Test4", result.Name);
+ Assert.AreEqual(TestOutcome.NotExecuted, result.Outcome);
+ Assert.AreEqual("Test was skipped", result.ErrorMessage);
+ }
+
+ ///
+ /// Test for deserialization with system output
+ ///
+ [TestMethod]
+ public void TestDeserializeWithSystemOutput()
+ {
+ // Deserialize the test results object with system output
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert test result information
+ var result = results.Results[0];
+ Assert.AreEqual("Test5", result.Name);
+ Assert.AreEqual("Standard output message", result.SystemOutput);
+ Assert.AreEqual("Standard error message", result.SystemError);
+ }
+
+ ///
+ /// Test for deserialization with multiple test suites
+ ///
+ [TestMethod]
+ public void TestDeserializeMultipleTestSuites()
+ {
+ // Deserialize the test results object with multiple test suites
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert results information
+ Assert.AreEqual("MultipleTests", results.Name);
+ Assert.HasCount(3, results.Results);
+
+ // Verify first test
+ var test1 = results.Results[0];
+ Assert.AreEqual("Test1", test1.Name);
+ Assert.AreEqual("Class1", test1.ClassName);
+ Assert.AreEqual(TestOutcome.Passed, test1.Outcome);
+
+ // Verify second test
+ var test2 = results.Results[1];
+ Assert.AreEqual("Test2", test2.Name);
+ Assert.AreEqual(TestOutcome.Failed, test2.Outcome);
+
+ // Verify third test
+ var test3 = results.Results[2];
+ Assert.AreEqual("Test3", test3.Name);
+ Assert.AreEqual("Class2", test3.ClassName);
+ Assert.AreEqual(TestOutcome.Passed, test3.Outcome);
+ }
+
+ ///
+ /// Test for deserialization with empty class name (DefaultSuite)
+ ///
+ [TestMethod]
+ public void TestDeserializeEmptyClassName()
+ {
+ // Deserialize the test results object with DefaultSuite
+ var results = JUnitSerializer.Deserialize(
+ """
+
+
+
+
+
+
+ """);
+ Assert.IsNotNull(results);
+
+ // Assert test result information - DefaultSuite should be converted to empty string
+ var result = results.Results[0];
+ Assert.AreEqual("Test1", result.Name);
+ Assert.AreEqual(string.Empty, result.ClassName);
+ }
+
+ ///
+ /// Test for round-trip serialization and deserialization
+ ///
+ [TestMethod]
+ public void TestRoundTrip()
+ {
+ // Create original test results
+ var original = new TestResults { Name = "RoundTripTests" };
+ original.Results.Add(
+ new TestResult
+ {
+ Name = "Test1",
+ ClassName = "TestClass",
+ Duration = TimeSpan.FromSeconds(1.5),
+ Outcome = TestOutcome.Passed,
+ SystemOutput = "Output message"
+ });
+ original.Results.Add(
+ new TestResult
+ {
+ Name = "Test2",
+ ClassName = "TestClass",
+ Duration = TimeSpan.FromSeconds(0.5),
+ Outcome = TestOutcome.Failed,
+ ErrorMessage = "Test failed",
+ ErrorStackTrace = "Stack trace here"
+ });
+
+ // Serialize to JUnit XML
+ var xml = JUnitSerializer.Serialize(original);
+
+ // Deserialize back
+ var deserialized = JUnitSerializer.Deserialize(xml);
+
+ // Verify results match
+ Assert.AreEqual(original.Name, deserialized.Name);
+ Assert.HasCount(original.Results.Count, deserialized.Results);
+
+ // Verify first test
+ var origTest1 = original.Results[0];
+ var deserTest1 = deserialized.Results[0];
+ Assert.AreEqual(origTest1.Name, deserTest1.Name);
+ Assert.AreEqual(origTest1.ClassName, deserTest1.ClassName);
+ Assert.AreEqual(origTest1.Duration.TotalSeconds, deserTest1.Duration.TotalSeconds, 0.001);
+ Assert.AreEqual(origTest1.Outcome, deserTest1.Outcome);
+ Assert.AreEqual(origTest1.SystemOutput, deserTest1.SystemOutput);
+
+ // Verify second test
+ var origTest2 = original.Results[1];
+ var deserTest2 = deserialized.Results[1];
+ Assert.AreEqual(origTest2.Name, deserTest2.Name);
+ Assert.AreEqual(origTest2.ClassName, deserTest2.ClassName);
+ Assert.AreEqual(origTest2.Outcome, deserTest2.Outcome);
+ Assert.AreEqual(origTest2.ErrorMessage, deserTest2.ErrorMessage);
+ Assert.AreEqual(origTest2.ErrorStackTrace, deserTest2.ErrorStackTrace);
+ }
}
From 5817a623e7d9007cd91a239968f1d6369bf23a3e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 19:54:16 +0000
Subject: [PATCH 5/6] Update documentation with format conversion examples
Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
---
ARCHITECTURE.md | 3 ++-
README.md | 22 ++++++++++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index ec929dd..104eccc 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -69,10 +69,11 @@ The `TrxSerializer` class is responsible for converting the domain model into TR
#### `JUnitSerializer`
-The `JUnitSerializer` class is responsible for converting the domain model into JUnit XML format:
+The `JUnitSerializer` class is responsible for converting between the domain model and JUnit XML format:
- Uses .NET's built-in XML serialization capabilities
- Produces JUnit XML files compatible with various CI/CD systems and testing tools
+- Can deserialize JUnit XML files back into the domain model
- Groups test results by class name into test suites
- Maps test outcomes to JUnit semantics (failure, error, skipped)
diff --git a/README.md b/README.md
index bd5b33f..cd75071 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,28 @@ File.WriteAllText(
JUnitSerializer.Serialize(results));
```
+### Converting Between Formats
+
+The library supports reading and converting between TRX and JUnit formats:
+
+```csharp
+using DemaConsulting.TestResults.IO;
+
+// Read JUnit XML file
+var junitXml = File.ReadAllText("junit-results.xml");
+var results = JUnitSerializer.Deserialize(junitXml);
+
+// Convert to TRX format
+var trxXml = TrxSerializer.Serialize(results);
+File.WriteAllText("converted.trx", trxXml);
+
+// Or convert TRX to JUnit
+var trxXml2 = File.ReadAllText("test-results.trx");
+var results2 = TrxSerializer.Deserialize(trxXml2);
+var junitXml2 = JUnitSerializer.Serialize(results2);
+File.WriteAllText("converted.xml", junitXml2);
+```
+
## Advanced Usage
### Capturing Standard Output
From d18dd1d74a6545f09f47b9147d280247301461bd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 16 Dec 2025 20:35:17 +0000
Subject: [PATCH 6/6] Fix variable naming: expand "deser" to "deserialized"
Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
---
.../IO/JUnitSerializerTests.cs | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs b/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
index 0914045..15a737f 100644
--- a/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
+++ b/test/DemaConsulting.TestResults.Tests/IO/JUnitSerializerTests.cs
@@ -692,20 +692,20 @@ public void TestRoundTrip()
// Verify first test
var origTest1 = original.Results[0];
- var deserTest1 = deserialized.Results[0];
- Assert.AreEqual(origTest1.Name, deserTest1.Name);
- Assert.AreEqual(origTest1.ClassName, deserTest1.ClassName);
- Assert.AreEqual(origTest1.Duration.TotalSeconds, deserTest1.Duration.TotalSeconds, 0.001);
- Assert.AreEqual(origTest1.Outcome, deserTest1.Outcome);
- Assert.AreEqual(origTest1.SystemOutput, deserTest1.SystemOutput);
+ var deserializedTest1 = deserialized.Results[0];
+ Assert.AreEqual(origTest1.Name, deserializedTest1.Name);
+ Assert.AreEqual(origTest1.ClassName, deserializedTest1.ClassName);
+ Assert.AreEqual(origTest1.Duration.TotalSeconds, deserializedTest1.Duration.TotalSeconds, 0.001);
+ Assert.AreEqual(origTest1.Outcome, deserializedTest1.Outcome);
+ Assert.AreEqual(origTest1.SystemOutput, deserializedTest1.SystemOutput);
// Verify second test
var origTest2 = original.Results[1];
- var deserTest2 = deserialized.Results[1];
- Assert.AreEqual(origTest2.Name, deserTest2.Name);
- Assert.AreEqual(origTest2.ClassName, deserTest2.ClassName);
- Assert.AreEqual(origTest2.Outcome, deserTest2.Outcome);
- Assert.AreEqual(origTest2.ErrorMessage, deserTest2.ErrorMessage);
- Assert.AreEqual(origTest2.ErrorStackTrace, deserTest2.ErrorStackTrace);
+ var deserializedTest2 = deserialized.Results[1];
+ Assert.AreEqual(origTest2.Name, deserializedTest2.Name);
+ Assert.AreEqual(origTest2.ClassName, deserializedTest2.ClassName);
+ Assert.AreEqual(origTest2.Outcome, deserializedTest2.Outcome);
+ Assert.AreEqual(origTest2.ErrorMessage, deserializedTest2.ErrorMessage);
+ Assert.AreEqual(origTest2.ErrorStackTrace, deserializedTest2.ErrorStackTrace);
}
}