This is the first post of a small series in which we will dig into the TestContext
class. I originally had this as one post, but part way through it was quite obvious that there was too much information to cover.
Many of you probably aren't familiar with all the builtin properties and methods that you have access to when using NUnit. When NUnit is executing tests, it runs tests in an execution context, which has information about the environment they are running in and on the tests themselves. The TestContext
gives you the ability to access that information as well as provide methods to improve your tests.
The TestContext
is a singleton that has a lot of useful static properties and methods. You can access the TestContext
in any project you are referencing the nunit framework. You can find all the basic information about the TestContext
here - TestContext
Test Context - Static Properties
CurrentContext
TestContext.CurrentContext
- Gets the context of the currently executing test. This context is created separately for each test before it begins execution. I'll go over the CurrentContext
in my next post: Overview of the TestContext in NUnit. Part 2: Using the CurrentContext.
Out
TestContext.Out
- Gets a TextWriter
used for sending output to the current test result. If using the default IResultWriter
, this will place the output into the XML result.
For instance, you might want to save the different greetings that we test.
[Test]
public void Hello()
{
string greeting = BlogSamples.HelloWorld.Hello("get-testy");
TestContext.Out.WriteLine($"Greeting: {greeting}");
Assert.That(greeting.Equals("Hello, get-testy.", StringComparison.Ordinal));
}
When this test gets executed, this would be the test case node inside the XML result.
<test-case id="0-1286" name="Post" fullname="BlogSamples.NUnit.Hello" methodname="Hello" classname="BlogSamples.NUnit.Hello" runstate="Runnable" seed="969179069" result="Passed" start-time="2017-11-07 19:14:25Z" end-time="2017-11-07 19:14:25Z" duration="0.012080" asserts="1">
<output><![CDATA[Greeting: get-testy]]></output>
</test-case>
This can be useful when you want to track variables that are created inside the test case. When you are debugging failures that occur on build servers, having these types of outputs can help you diagnose failures easier.
Error
TestContext.Error
- Gets a TextWriter
used for sending error output intended for immediate display. Use this to send messages to the Error output stream.
For instance, you might want to write out an Error that occurred but not stop the execution of the test while still capturing the errors.
for(int i = 0; i < 5; i++)
{
try
{
//Perform action and suppress exceptions
}
catch(System.Exception exception)
{
TestContext.Error.WriteLine($"Error: {exception.ToString()}");
}
}
Progress
TestContext.Progress
- Gets a TextWriter
used for sending normal (non-error) output intended for immediate display. Use to send information out to the immediate console output. If you have a lot going on in your tests, it's good practice to log what is occurring during your test execution.
For instance, you might want to track actions that are occurring in a loop.
for(int i = 0; i < 5; i++)
{
//Some Action
TestContext.Progress.WriteLine($"Action number {i} has occurred.");
}
TestParameters
Test parameters are going to need their own post to show all the things that you can do. However, I will give a quick overview here just to give a taste of what you can do with the test parameters.
They can be supplied in two different ways: through the nunit console and through the nunit3 visual studio adapter. The static TestParameters
property returns an object representing those passed-in parameters. The TestParameters
class has the following methods and properties.
properties:
Count
- The number of parameters.Names
- A collection of the names of the supplied parameters.this[string name]
- The value of a parameter. In Vb, use Item.
methods:
Exists(string name)
- Returns true if a parameter of that name exists.Get(string name)
- Returns the same value as the indexer.Get<T>(string name, T defaultValue)
- Returns the value of the parameter converted from a string to type T or the specified default if the parameter doesn't exist. Throws an exception if conversion fails.
To define the test parameters in visual studio, this would be what a simple *.runsettings
file would look like.
<RunSettings>
<TestRunParameters>
<Parameter name="appUrl" value="http://localhost" />
</TestRunParameters>
</RunSettings>
Then in our test, we might access the appUrl
value like this.
TestContext.Out.WriteLine($"{TestContext.Parameters["appUrl"]}");
Test Context - Static Methods
Write and WriteLine
Writes text to the current test result and writes text to the current test result but followed by a newline.
Using the TestContext.Write
and TestContext.WriteLine
is preferred to using System.Console.Write
and System.Console.WriteLine
due to the System
class redirecting the output to standard out. In basic scenarios, you often won't run into a problem with using the System
classes, but it's best practice to use the TestContext
classes wherever possible.
AddFormatter
If you have custom objects that you are comparing inside constraints, you can use a custom formatter to change the output to make it more human readable.
AddFormatter(ValueFormatter formatter);
AddFormatter(ValueFormatterFactory formatterFactory);
We can use either a ValueFormatter
or a ValueFormatterFactory
, which are delegates, to format our objects.
For instance, we have a basic data transfer object (basicDTO) that we want to compare.
class basicDTO {
public string Name { get; set; }
}
private string FormatBasicDTO(basicDTO val)
{
return $"Name: {val.Name}";
}
[Test]
public void Post()
{
TestContext.AddFormatter<basicDTO>(val => FormatBasicDTO((basicDTO)val));
basicDTO firstDTO = new basicDTO() { Name = "First DTO" };
basicDTO secondDTO = new basicDTO() { Name = "Second DTO" };
Assert.That(firstDTO, Is.EqualTo(secondDTO));
}
When we run this, since we did not override the ToEquals
for the comparer on the basicDTO, this test will fail.
With the ValueFormatter, we get:
1) Failed : BlogSamples.NUnit.HelloWorld.Hello
Expected: <BlogSamples.NUnit.HelloWorld.Hello+basicDTO>
But was: <BlogSamples.NUnit.HelloWorld.Hello+basicDTO>
Without the ValueFormatter, we get:
1) Failed : BlogSamples.NUnit.HelloWorld.Hello
Expected: Name: Second DTO
But was: Name: First DTO
This is useful only for when you don't have the ability to override the ToString
on the object you are comparing. However, best practice would be to just give a useful ToString
method and use the built in formatters.
AddTestAttachment
Attaches a file, with optional description, to the current test. The file will be attached to the test result in the xml report.
AddTestAttachment(string filePath, string description = null);
You might want to do this when:
- You want to see what state a page is in when a front end test fails. Take a screenshot and add it as an attachment so you can view afterwards.
- You are building up an in-memory
XmlDocument
orDataTable
and you want to see what the data looks like in a human readable format.
Here would be an example of saving an in-memory XmlDocument
to a file and attaching it to the Xml Test Result. This is what you might have inside your test.
//Get the file path for where we want to save the file
string xmlData = System.IO.Path.Combine(TestContext.CurrentContext.WorkDirectory, "xmlData.xml");
//Build up your in-memory XmlDocument
XmlDocument doc = new XmlDocument();
XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
XmlElement root = doc.DocumentElement;
doc.InsertBefore(xmlDeclaration, root);
XmlElement getTesty = doc.CreateElement(string.Empty, "get-testy", string.Empty);
doc.AppendChild(getTesty);
XmlText text1 = doc.CreateTextNode("text");
getTesty.AppendChild(text1);
//Save your XmlDocument to the actual file on disc
doc.Save(xmlData);
//Attach the file to the Test Result
TestContext.AddTestAttachment(xmlData, "In memory XML Data");
When you have that block of code execute inside your tests, you will then have an extra file generated and and extra section added to your TestResult.xml.
<!-- attachment node is added to the TestResult.xml now -->
<attachment>
<filePath>E:\SourceCode\BlogSamples\xmlData.xml</filePath>
<description><![CDATA[In memory XML Data]]></description>
</attachment>
That covers all of the static properties and methods. I would try playing around with the TestContext
and see what you can do to improve your existing tests.
Moving forward, when building new tests, you now have a few more tools in your toolbox for writing quality tests!