Logging is a very important strategy to programming because it helps us locate issues when we inevitably have to debug a problem.
There are many ways to handle your Logging strategy, like using an existing Logging library like Log4Net, or a service like Loggly or Log Stash, but for this course we're just going to write to a text [.txt
] file.
It may not sound as awesome as "Logger as a Service", but your CI/CD pipelines can easily work with artifacts your test runs generate and these log files are a piece of cake.
Another benefit is seeing how to read and write to files using C sharp, which has a lot of applications outside of just logging.
Let's dive in.
The first thing we'll want to do is to store our log files into a TestResults directory.
Now, we're going to create a Framework class to handle a lot of things like our singletons and setups and whatnot.
We're going to start in our Driver
class because we actually want to get this Path.GetFullPath(@"../../../../"
This is actually used in a lot of places. We'll copy this.
Then right-click on Framework > New File, and we'll call it “FW.cs” (this stands for Framework).
Now inside of here is where we're going to be putting what we just copied.
So public class FW
for framework.
In public static string
we're going to call this “WORKSPACE_DIRECTORY” and his is going to equal what we copied.
Let's go over here and bring this in (System.IO). There we go.
Now we're going to create the test directory that's going to hold all of our stuff.
So public static DirectoryInfo
and we're going to call this “CreateTestResultsDirectory”.
Then var testResultsDirectory
is going to be equal to our WORKSPACE_DIRECTORY + "/TestResults"
So, that's where our directory is going to live.
We're also going to check it as well.
We're going to say if this directory already exists, then we want to actually delete it, we want to clear it out, because that means that there was an old test run that we don't want anymore.
So, we’ll use Delete
— we want to get rid of the test directory and then recursive
we're going to set that to true
— that means it's going to delete not just this directory, but all of the subdirectories and files within it as well.
Now we can return Directory.CreateDirectory
and we'll make sure to create that test directory.
namespace Framework
{
public class FW
{
public static string WORKSPACE_DIRECTORY = Path.GetFullPath(@"../../../../");
public static DirectoryInfo CreateTestResultsDirectory()
{
var testResultsDirectory = WORKSPACE_DIRECTORY + "TestResults";
if (Directory.Exists(testResultsDirectory))
{
Directory.Delete(testResultsDirectory, recursive: true);
}
return Directory.CreateDirectory(testResultsDirectory);
}
}
}
Now we're going to go into Framework and make another directory.
Inside of this we're going to call this one “Logging” and inside of here we'll make one called a “Logger.cs”.
We’re going to hold the _filepath
variable.
Then let's get our constructor [Logger
] going, pass in a test name and then a filepath as well.
We're going to have our private filepath [_filepath
] equal the filepath that we pass in.
namespace Framework.Logging
{
public class Logger
{
private readonly string _filepath;
public Logger(string testName, string filepath)
{
_filepath = filepath;
}
}
There we go.
So using
this open file now we can now do things to it.
We can say log.WriteLine
and let's log with something to it right now.
Let's say “Starting timestamp” and we're going to have this be the date and time.
using (var log = File.CreateText(_filepath))
{
log.WriteLine($"Starting timestamp: {DateTime.Now.ToLocalTime()}");
}
Whenever we make a new instance of Logger, it's going to make a new file and then print the starting timestamp for the Logger file.
Now let's make two private
methods.
The first one's going to be a WriteLine
.
This is going to be the main one that we'll use and this one will write a new line to the log file using File.AppendText
.
And we're going to be using the same file path from before and whatever text we pass into this.
private void WriteLine(string text)
{
using (var log = File.AppendText(_filepath))
{
log.WriteLine(text);
}
}
The next private
method is just called a Write
.
This one is similar to WriteLine
but instead of making a brand-new line and then writing the text to it, it will append on the line it's currently on. This is nice in certain cases as well.
It’s still the same file path we're working with, and then whatever text we pass in.
private void Write(string text)
{
using (var log = File.AppendText(_filepath))
{
log.Write(text);
}
}
Scroll it back to the top here, I actually forgot to add this log.WriteLine
— we want to put our name, the name of the tests that we're doing.
using (var log = File.CreateText(_filepath))
{
log.WriteLine($"Starting timestamp: {DateTime.Now.ToLocalTime()}");
log.WriteLine($"Test: {testName}");
}
The next thing we'll do is to actually add different types of Loggings.
You may have something that you want to:
So, we're going to add those next.
You'll see they have a very common pattern, but once you see them writing to the actual log file, they'll make a lot more sense why we're doing it this way.
The first one is Info
.
Now we can say WriteLine
using our private one.
public void Info(string message)
{
WriteLine($"[INFO]: {message}");
}
Next up is a Step
.
Note – there are 4 spaces before [STEP]
.
public void Step(string message)
{
WriteLine($" [STEP]: {message}");
}
Next one up, we're going to have to be a Warning
.
public void Warning(string message)
{
WriteLine($"[WARNING]: {message}");
}
After that, the next one is an Error
.
public void Error(string message)
{
WriteLine($"[ERROR]: {message}");
}
And the last one is a fatality [Fatal
] — that means that there's a break or something that is gone terribly wrong and we need to log it at the highest level.
public void Fatal(string message)
{
WriteLine($"[FATAL]: {message}");
}
Now we'll go back to our Framework class [FW.cs
], because that's where we will be setting the Logger.
But in order to set it, we're going to be needing something from the NUnit Framework. Right now, our Framework project doesn't have a reference to NUnit, so we need to add that.
I'm going to use PackSharp to do this.
So, _PackSharp > PackSharp: Add Package _(to the Framework - FW.cs ) — we need to add NUnit (the first one, not the Test Adapter)
Then we'll remove a package.
So, we'll say: _PackSharp > PackSharp: Remove Package (from Royal.Tests) _— we need to remove NUnit.
Then to make sure everything is set up we do a dotnet clean
, dotnet restore
and dotnet build
.
So, we added NUnit to our Framework (FW.cs) project and we removed NUnit from our Royal.Test project and everything is building well.
Okay, now we can add our SetLogger
method.
We're going to start off by saying the testResultsDirectory
and we'll just say “Dir” for short.
The current running test’s name is equal to TestContext.CurrentContext.Test.Name
(and this is coming from NUnit) — so the current running test will know exactly the name of that test, and now our test named variable equals that.
And then the fullPath
— this will effectively make a directory within the test results directory and the new directory will be, whatever the name of the test that's currently running is.
public static void SetLogger()
{
var testResultsDir = WORKSPACE_DIRECTORY + "TestResults";
var testName = TestContext.CurrentContext.Test.Name;
var fullPath = $"{testResultsDir}/{testName}";
}
Now, we want to hold the current test directory as a variable because this is something that we're going to want to track throughout the test run, so we use [ThreadStatic]
.
We’re going to call this the current test directory.
[ThreadStatic]
public static DirectoryInfo CurrentTestDirectory;
Let's scroll back down [to the SetLogger
method] and now we can set our current test directory.
So, the CurrentTestDirectory
is going to equal Directory.CreateDirectory
using that full path.
CurrentTestDirectory = Directory.CreateDirectory(fullPath);
The last thing we'll need to do up here, is to hold that instance of Logger
as well.
We’ll create our private _logger
.
[ThreadStatic]
private static Logger _logger;
Okay, let's bring Logger
in.
Perfect.
Then we want to have a public version of this as well.
We’ll call it “Log” and it's going to be equal to Logger
, unless it's null, in which case we're going to throw a new NullReferenceException
.
public static Logger Log => _logger ?? throw new NullReferenceException("_logger is null. SetLogger() first.");
Now back down here [in our SetLogger
method], we can now set that _logger
to a value.
public static void SetLogger()
{
var testResultsDir = WORKSPACE_DIRECTORY + "TestResults";
var testName = TestContext.CurrentContext.Test.Name;
var fullPath = $"{testResultsDir}/{testName}";
CurrentTestDirectory = Directory.CreateDirectory(fullPath);
_logger = new Logger(testName, CurrentTestDirectory.FullName + "/log.txt");
}
It seems like a lot.
But really, what we're doing is we're making sure that each test has its own instance of Logger
, hence the ThreadStatic
, and then also us holding the instance of Logger
plus the test directory as well.
Quiz
The quiz for this chapter can be found in section 8.2