If you’re getting started with .NET or you’ve done some testing but want to know how to put it all together and also learn some additional tools then the new xUnit.net testing path from Pluralsight may be of interest (you can also get started viewing for free with a trial).

The path currently has the following individual courses (including some of my courses) taking you right from the basics of xUnit.net to more advanced techniques:

• Testing .NET Code with xUnit.net: Getting Started
• Mocking in .NET Core Unit Tests with Moq: Getting Started
• Creating Maintainable Contexts for Automated Testing
• Writing Testable Code
• Building a Pragmatic Unit Test Suite
• Improving Unit Tests with Fluent Assertions
• Better .NET Unit Tests with AutoFixture: Get Started
• Approval Tests for .NET

If you’re already skilled with xUnit.net you may find some of the other courses in the path useful.

SHARE:

Sometimes the assertions that come bundled with a testing framework are suboptimal in that they do not provide test failure messages that allow easier understanding of why/where the test failed.

If the test failure message does not provide enough information, it may be necessary to run the test in debug mode just to find out what went wrong before fixing it. This test debugging step is wasted time.

Using the built-in assertions can also be suboptimal from a code readability point of view, though this can be a matter of personal preference.

The Fluent Assertions library aims to solve these two problems by:

• Providing better, more descriptive test failure messages; and
• Providing a more fluent, readable  syntax for assertions

Let’s take a look at some examples. Note that Fluent Assertions is an “add on” to whatever testing framework you are using (NUnit, xUnit.net, etc.).

The following test (using NUnit) shows a simple case:

public class CreditCardApplication
{
public string Name { get; set; }
public int Age { get; set; }
public decimal AnnualGrossIncome { get; set; }

public int CalculateCreditScore()
{
int score = 0;

if (Age > 30)
{
score += 10;
}

if (AnnualGrossIncome < 50_000)
{
score += 30;
}
else
{
score += 30;
}

return score;
}
}


[Test]
public void NUnitExample()
{
var application = new CreditCardApplication
{
Name = "Sarah",
Age = 31,
AnnualGrossIncome = 50_001
};

Assert.That(application.CalculateCreditScore(), Is.EqualTo(50));
}


When this test fails using the built-in NUnit asserts, the failure message is:

Test Outcome:    Failed

Result Message:
Expected: 50
But was:  40


Notice in the preceding test failure message we don’t have any context about what is failing or what the number 50 and 40 represent. While well-named tests can help with this, it can be helpful to have additional information, especially when there are multiple asserts in a single test method.

The same test in xUnit.net:

[Fact]
public void XUnitExample()
{
var application = new CreditCardApplication
{
Name = "Sarah",
Age = 31,
AnnualGrossIncome = 50_001
};

Assert.Equal(50, application.CalculateCreditScore());
}


Produces the message:

Test Outcome:    Failed

Result Message:
Assert.Equal() Failure
Expected: 50
Actual:   40


Once again there is no additional context about the failure.

The same test written using Fluent Assertions would look like the following:

[Fact]
public void XUnitExample_WithFluentAssertions()
{
var application = new CreditCardApplication
{
Name = "Sarah",
Age = 31,
AnnualGrossIncome = 50_001
};

application.CalculateCreditScore().Should().Be(50);
}


Now when the test fails, the message looks like the following:

Test Outcome:    Failed
Result Message:
Expected application.CalculateCreditScore() to be 50, but found 40.


Notice the failure is telling us the method (or variable) name that is being asserted on – in this example the CalculateCreditScore method.

Optionally you can also add a “because” to further clarify failures:

[Fact]
public void XUnitExample_WithFluentAssertions_Because()
{
var application = new CreditCardApplication
{
Name = "Sarah",
Age = 31,
AnnualGrossIncome = 50_001
};

application.CalculateCreditScore().Should().Be(50, because: "an age of {0} should be worth 20 points and an income of {1} should be worth 30 points.", application.Age, application.AnnualGrossIncome);
}


This would now produce the following failure message:

Test Outcome:    Failed
Result Message:
Expected application.CalculateCreditScore() to be 50 because an age of 31 should be worth 20 points and an income of 50001 should be worth 30 points., but found 40.


Notice the “because” not only gives a richer failure message but also helps describe the test. While you probably wouldn’t use the because feature on every assert, you could use it to clarify tests that may not be obvious at first sight or that represent complex domain logic or algorithms. You should also be aware that the because text may need modifying if the business logic changes and this may introduce an additional maintenance cost.

If you want to learn more about Fluent Assertions, check out my express Pluralsight course Improving Unit Tests with Fluent Assertions which you can get access to with a Pluralsight free trial by clicking the banner below.

SHARE:

Knowing what you need to know is hard. Sometimes harder than the learning itself.

Many years ago I was getting started with .NET v1 and .NET unit testing, Agile had recently been “invented” and I had a printout of the agile manifesto on the office wall. I was also learning Test Driven Development (TDD), mocking, unit testing frameworks, assertions, data driven testing.

I remember it being a little overwhelming at times, so much to learn with fragments of information scattered around but no clearly defined path to follow to get where I knew I wanted to be: proficient and efficient in writing high quality, tested and testable code.

Today things are a little easier but there can still be the: “I don’t know what I need to know”.

This is where skills paths from Pluralsight can be super helpful. I wish I had had them all those years ago.

A path is a curated collection of courses in a specific order to get you to where you need to be for a specific learning goal.

I’m super proud to have contributed to the C# Unit Testing with NUnit Pluralsight path which at the time of writing you can start to watch for free with a Pluralsight free trial.

While it’s certainly possible that you could find the information and learn the topics yourself, you would also waste so much time in getting the information from disparate sources and trying to “meta learn” what it is you don’t know. Ultimately it depends on how much free time you have and how efficient you want to be at learning. You should always keep the end goal in mind and weigh up the costs/benefits/risks of the different ways of getting to that goal. If you want to learn to “how to write clean, testable code, all the way from writing your first test to mocking out dependencies to developing a pragmatic suite of unit tests for your application” then the C# Unit Testing with NUnit path may be your most efficient approach to get to your goal.

SHARE:

(This post refers to Azure Functions v2)

One way to test Azure Functions that use Event Grid triggers is to run the Function App locally and then get Azure in the cloud to invoke the function running on the local machine. As an example, suppose you want to use Event Grid to improve the reliability and responsiveness of Blob Storage processing. To do this the documentation suggests the use of ngrok. Now when a blob is added to a container in the cloud, the locally running function on the dev machine will be invoked via ngrok.

There is a somewhat simpler solution that allows you to invoke the Event Grid triggered function locally.

This approach bypasses Event Grid completely, so it is not a substitute for proper end-to-end testing, it’s more a development-time testing & debugging tool.

## Manually Running Non HTTP-Triggered Azure Functions

You can manually trigger a non HTTP-triggered function (such as a timer triggered or Event Grid triggered function) via a special HTTP endpoint.

The endpoint is of the format: {host}/admin/functions/{function name}

For example, take the following function (which was also used in the post Improving Azure Functions Blob Trigger Performance and Reliability - Part 3: Using Event Grid to Respond to New Blobs):

public static class ProcessFoodBlobsEventGrid
{
private static readonly string[] _meats = { "steak", "chicken", "venison" };

[FunctionName("ProcessFoodBlobsEventGrid")]
public static void Run(
[EventGridTrigger]EventGridEvent blobCreatedEvent,
[Blob("{data.url}")] string foods, // assumes small blob size so using string not stream
[Blob("{data.url}.vegetarian")] out string vegetarian,
[Blob("{data.url}.nonvegetarian")] out string nonVegetarian,
ILogger log)
{
log.LogInformation("Processing a blob created event");

StorageBlobCreatedEventData createdEvent = ((JObject)blobCreatedEvent.Data).ToObject<StorageBlobCreatedEventData>();

log.LogInformation($"Blob: {createdEvent.Url}"); log.LogInformation($"Api operation: {createdEvent.Api}");

vegetarian = null;
nonVegetarian = null;

string[] foodLines = foods.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);

foreach (var food in foodLines)
{
var isMeat = _meats.Contains(food);

if (isMeat)
{
nonVegetarian += food + Environment.NewLine;
}
else
{
vegetarian += food + Environment.NewLine;
}
}
}
}



The preceding function when running locally in development would have the special URL: http://localhost:7071/admin/functions/ProcessFoodBlobsEventGrid

If you had a timer-triggered function called HerdCats that you wanted to manually invoke (so you didn’t have to wait for the next timed invocation) the special URL would be:http://localhost:7071/admin/functions/HerdCats

Note: when running locally in development you do not have to authenticate. If you wanted to manually invoke a deployed function in Azure, you need to provide an x-functions-key header that contains the function master key.

## Manually Invoking an Event Grid Triggered Azure Function

When using the special URL to invoke a function, you can also provide data to be passed to the function. The type of data passed will depend on the trigger type of the function that you are invoking.

To provide data to the function, a JSON payload can be posted to the special URL. The data that is passed to the function is contained in a JSON property called “input”:

{
"input": "trigger data goes here"
}


If the Event Grid triggered function will be invoked by a new blob event, the contents of this input property must match the event schema for an Azure Blob Storage event.

An example of event JSON (taken from the Microsoft documentation):

[{
"topic": "/subscriptions/{subscription-id}/resourceGroups/Storage/providers/Microsoft.Storage/storageAccounts/xstoretestaccount",
"subject": "/blobServices/default/containers/testcontainer/blobs/testfile.txt",
"eventType": "Microsoft.Storage.BlobCreated",
"eventTime": "2017-06-26T18:41:00.9584103Z",
"id": "831e1650-001e-001b-66ab-eeb76e069631",
"data": {
"api": "PutBlockList",
"clientRequestId": "6d79dbfb-0e37-4fc4-981f-442c9ca65760",
"requestId": "831e1650-001e-001b-66ab-eeb76e000000",
"eTag": "0x8D4BCC2E4835CD0",
"contentType": "text/plain",
"contentLength": 524288,
"blobType": "BlockBlob",
"url": "https://example.blob.core.windows.net/testcontainer/testfile.txt",
"sequencer": "00000000000004420000000000028963",
"storageDiagnostics": {
"batchId": "b68529f3-68cd-4744-baa4-3c0498ec19f0"
}
},
"dataVersion": "",
}]


When testing the function outlined earlier, the first thing to do is ensure that there is a blob in the local blob container that will be read by the function by way of the blob input binding: [Blob("{data.url}")] string foods.

For example, in the Storage Emulator a blob called in.txt can be uploaded to the food-in container.

Now the new blob event data JSON needs to be modified, specifically the data.url property needs to contain the URL to the local blob: http://127.0.0.1:10000/devstoreaccount1/food-in/in.txt

A modified version with updated data.url would be as follows:

[{
"topic": "/subscriptions/{subscription-id}/resourceGroups/Storage/providers/Microsoft.Storage/storageAccounts/xstoretestaccount",
"subject": "/blobServices/default/containers/testcontainer/blobs/testfile.txt",
"eventType": "Microsoft.Storage.BlobCreated",
"eventTime": "2017-06-26T18:41:00.9584103Z",
"id": "831e1650-001e-001b-66ab-eeb76e069631",
"data": {
"api": "PutBlockList",
"clientRequestId": "6d79dbfb-0e37-4fc4-981f-442c9ca65760",
"requestId": "831e1650-001e-001b-66ab-eeb76e000000",
"eTag": "0x8D4BCC2E4835CD0",
"contentType": "text/plain",
"contentLength": 524288,
"blobType": "BlockBlob",
"url": "http://127.0.0.1:10000/devstoreaccount1/food-in/in.txt",
"sequencer": "00000000000004420000000000028963",
"storageDiagnostics": {
"batchId": "b68529f3-68cd-4744-baa4-3c0498ec19f0"
}
},
"dataVersion": "",
}]


The next step is to remove the surrounding [], and replace the with . Then paste the resulting JSON into the input property:

{
"input": "
{
'topic': '/subscriptions/{subscription-id}/resourceGroups/Storage/providers/Microsoft.Storage/storageAccounts/xstoretestaccount',
'subject': '/blobServices/default/containers/oc2d2817345i200097container/blobs/oc2d2817345i20002296blob',
'eventType': 'Microsoft.Storage.BlobCreated',
'eventTime': '2017-06-26T18:41:00.9584103Z',
'id': '831e1650-001e-001b-66ab-eeb76e069631',
'data': {
'api': 'PutBlockList',
'clientRequestId': '6d79dbfb-0e37-4fc4-981f-442c9ca65760',
'requestId': '831e1650-001e-001b-66ab-eeb76e000000',
'eTag': '0x8D4BCC2E4835CD0',
'contentType': 'application/octet-stream',
'contentLength': 524288,
'blobType': 'BlockBlob',
'url': 'http://127.0.0.1:10000/devstoreaccount1/food-in/in.txt',
'sequencer': '00000000000004420000000000028963',
'storageDiagnostics': {
'batchId': 'b68529f3-68cd-4744-baa4-3c0498ec19f0'
}
},
'dataVersion': '',
}
"
}


Now this JSON can be POSTed to the special URL: in the case of the example in this post the URL would be: http://localhost:7071/admin/functions/ProcessFoodBlobsEventGrid

The following screenshot shows posting using Postman:

Posting will cause the Event Grid triggered function to be invoked and the JSON contained inside the input property will be passed to the trigger input EventGridEvent blobCreatedEvent object. The function will execute and read in the blob called “in.txt”.

SHARE:

When the result you want to check is a collection, you can use NUnit to assert that it has the expected number of items or is empty, that all items are unique, that specific items do/not exist, and that items exist that satisfy some condition or predicate.

## Asserting on the Number of Items in a Collection with NUnit Asserts

var names = new[] { "Sarah", "Amrit", "Amanda", "Sarah" };

Assert.That(names, Has.Exactly(4).Items); // pass
Assert.That(names, Is.Empty); // fail
Assert.That(names, Is.Not.Empty); // pass


## Asserting That All Items in a Collection are Unique with NUnit Asserts

Assert.That(names, Is.Unique); // fail - 2 Sarah items exist


## Asserting That An Item Does or Does Not Exist in a Collection with NUnit Asserts

Assert.That(names, Contains.Item("Sarah")); // pass

// Alternative syntax
Assert.That(names, Does.Contain("Sarah")); // pass
Assert.That(names, Does.Not.Contain("Arnold")); // pass


## Asserting That An Item Appears a Specified Number Of Times in a Collection with NUnit Asserts

Assert.That(names, Has.Exactly(1).EqualTo("Sarah")); // fail
Assert.That(names, Has.Exactly(2).EqualTo("Sarah")); // pass
Assert.That(names, Has.Exactly(2).EqualTo("Sarah")
.And.Exactly(1).EqualTo("Amrit")); // pass


## Asserting That All Items In a Collections Satisfy a Predicate/Condition with NUnit Asserts

Assert.That(names, Is.All.Not.Null); // pass
Assert.That(names, Is.All.Contains("a")); // fail lowercase a
Assert.That(names, Is.All.Contains("a").IgnoreCase); // pass
Assert.That(names, Is.All.Matches<string>(name => name.ToUpperInvariant().Contains("A"))); // pass
Assert.That(names, Is.All.Matches<string>(name => name.Length > 4)); // pass


## Asserting That Only One Item In a Collection Satisfies a Predicate with NUnit Asserts

Assert.That(names, Has.Exactly(1).Matches<string>(name => name.Contains("mri"))); // pass
Assert.That(names, Has.Exactly(1).Matches<string>(name => name.Contains("ara"))); // fail (2 Sarah items exist)


To learn more about NUnit 3 check out my Introduction to .NET Testing with NUnit 3 Pluralsight course to learn everything you need to know to get started, including asserts, categories, data-driven tests, customizing NUnit, and reducing duplicate test code.

SHARE:

When asserting on equality using the EqualConstraint you may not always get the behaviour you want depending on what objects are being asserted on. This can be influenced by whether or not the objects are value or reference types and if the type implements or overrides methods such as IEquatable<T> or object.Equals overrides.

## Asserting on Value Type Equality with NUnit

int a = 42;
int b = 42;

Assert.That(a, Is.EqualTo(b)); // pass - values are same, ints are structs with value semantics
Assert.That(a, Is.SameAs(b)); // fail - a and b do not point to the same object in memory

int c = a;

Assert.That(c, Is.EqualTo(a)); // pass - values are same


## Asserting on Reference Type Equality with NUnit

By default, 2 instances of a reference type will not pass an equality assert:

class Person
{
public string Name { get; set; }
}

Person p1 = new Person { Name = "Sarah" };
Person p2 = new Person { Name = "Sarah" };

Assert.That(p1, Is.EqualTo(p2)); // fail, Person is class with reference semantics


## Asserting That Two References Point to the Same Object with NUnit

If you want to assert that 2 object references point to the same object you can use the SameAsConstraint:

Assert.That(p1, Is.SameAs(p2)); // fail, p1 and p2 point to different objects in memory

Person p3 = p1;

Assert.That(p3, Is.SameAs(p1)); // pass, p3 and p1 point to same object in memory
Assert.That(p3, Is.Not.SameAs(p2)); // pass, p3 and p2 point to different objects in memory


## Customizing Equality Asserts with NUnit

There are a number of ways to influence how NUnit performs equality assertions including implementing IEquatable<T>:

class Employee : IEquatable<Employee>
{
public string Name { get; set; }

public bool Equals(Employee other)
{
if (other is null)
{
return false;
}

return Name == other.Name;
}
}

Employee e1 = new Employee { Name = "Sarah" };
Employee e2 = new Employee { Name = "Sarah" };

Assert.That(e1, Is.EqualTo(e2)); // pass - IEquatable<Employee>.Equals implementation is used


To learn more about NUnit 3 check out my Introduction to .NET Testing with NUnit 3 Pluralsight course to learn everything you need to know to get started, including asserts, categories, data-driven tests, customizing NUnit, and reducing duplicate test code.

SHARE:

If you are asserting that a value is equal to something and you want to specify some tolerance you can do so.

## Specifying a Range for Values with NUnit Asserts (e.g. int)

var i = 42;

Assert.That(i, Is.EqualTo(40)); // fail

Assert.That(i, Is.EqualTo(40).Within(2)); // pass

Assert.That(i, Is.EqualTo(40).Within(1)); // fail "Expected: 40 +/- 1"


## Specifying a Range as a Percentage with NUnit Asserts

In addition to specifying a range tolerance as a fixed value you can also specify it as a percentage:

Assert.That(i, Is.EqualTo(40).Within(5).Percent); // pass

Assert.That(i, Is.EqualTo(40).Within(4).Percent); // fail "Expected: 40 +/- 4 Percent"


## Specifying a Range for DateTime Objects with NUnit Asserts

When working with DateTimes you can specify the tolerance as a TimeSpan instance:

var newYearsDay2019 = new DateTime(2019, 1, 1);

Assert.That(newYearsDay2019, Is.EqualTo(new DateTime(2019, 1, 2)).Within(TimeSpan.FromDays(1))); // pass



Or instead of using a TimeSpan you can use one of the convenience modifiers:

Assert.That(newYearsDay2019, Is.EqualTo(new DateTime(2019, 1, 2)).Within(1).Days); // pass

Assert.That(newYearsDay2019, Is.EqualTo(new DateTime(2019, 1, 2)).Within(24).Hours); // pass
Assert.That(newYearsDay2019, Is.EqualTo(new DateTime(2019, 1, 2)).Within(23).Hours); // fail

var numberOfMinutesInADay = 24 * 60;
Assert.That(newYearsDay2019, Is.EqualTo(new DateTime(2019, 1, 2)).Within(numberOfMinutesInADay).Minutes); // pass
Assert.That(newYearsDay2019, Is.EqualTo(new DateTime(2019, 1, 2)).Within(numberOfMinutesInADay - 1).Minutes); // fail "Expected: 2019-01-02 00:00:00 +/- 23:59:00"

// Also Within(n).Seconds .Milliseconds and .Ticks

To learn more about NUnit 3 check out my Introduction to .NET Testing with NUnit 3 Pluralsight course to learn everything you need to know to get started, including asserts, categories, data-driven tests, customizing NUnit, and reducing duplicate test code.

SHARE:

When creating Azure Functions that are triggered by an HTTP request, you may want to write unit tests for the function Run method. These unit tests can be executed outside of the Azure Functions runtime, just like testing regular methods.

If your HTTP-triggered function takes as the function input an HttpRequest (as opposed to an automatically JSON-deserialized class) you may need to provide request data in your test.

As an example, consider the following code snippet that defines an HTTP-triggered function.

[FunctionName("Portfolio")]
[return: Queue("deposit-requests")]
public static async Task<DepositRequest> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "portfolio/{investorId}")]HttpRequest req,
[Table("Portfolio", InvestorType.Individual, "{investorId}")] Investor investor,
string investorId,
ILogger log)
{
log.LogInformation($"C# HTTP trigger function processed a request."); string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); log.LogInformation($"Request body: {requestBody}");

var deposit = JsonConvert.DeserializeObject<Deposit>(requestBody);

// etc.
}


If the  the preceding code is executed in a test, some content needs to be provided to be used when accessing req.Body. To do this using Moq a mock HttpRequest can be created that returns a specified Stream instance for req.Body.

If you want to create a request body that contains a JSON payload, you can use the following helper method in your tests:

private static Mock<HttpRequest> CreateMockRequest(object body)
{
var ms = new MemoryStream();
var sw = new StreamWriter(ms);

var json = JsonConvert.SerializeObject(body);

sw.Write(json);
sw.Flush();

ms.Position = 0;

var mockRequest = new Mock<HttpRequest>();
mockRequest.Setup(x => x.Body).Returns(ms);

return mockRequest;
}


As an example of using this method in a test:

[Fact]
public async Task ReturnCorrectDepositInformation()
{
var deposit = new Deposit { Amount = 42 };
var investor = new Investor { };

Mock<HttpRequest> mockRequest = CreateMockRequest(deposit);

DepositRequest result = await Portfolio.Run(mockRequest.Object, investor, "42", new Mock<ILogger>().Object);

Assert.Equal(42, result.Amount);
Assert.Same(investor, result.Investor);
}


When the preceding test is run, the function run method will get the contents of the memory stream that contains the JSON.

To learn more about using Moq to create/configure/use mock objects check out my Mocking in .NET Core Unit Tests with Moq: Getting Started Pluralsight course. or to learn more about MemoryStream and how to work with streams in C# check out my Working with Files and Streams course.

SHARE:

I recently received a message related to my Mocking in .NET Core Unit Tests with Moq: Getting Started Pluralsight course asking how to set the values of ref parameters.

As a (somewhat contrived) example, consider the following code:

public interface IParser
{
bool TryParse(string value, ref int output);
}

public class Thing
{
private readonly IParser _parser;

public Thing(IParser parser)
{
_parser = parser;
}

public string ConvertStringIntToHex(string number)
{
int i = 0;

if (_parser.TryParse(number, ref i))
{
return i.ToString("X");
}

throw new ArgumentException("The value supplied cannot be parsed into an int.", nameof(number));
}
}


The Thing class requires an IParser to be able to work. In a test, a mocked version of an IParser can be created by Moq as the following initial test demonstrates:

[Fact]
public void ReturnHex_Fail_NoSetup()
{
var mockParser = new Mock<IParser>();

var sut = new Thing(mockParser.Object);

var result = sut.ConvertStringIntToHex("255"); // fails with ArgumentException

Assert.Equal("FF", result);
}


The preceding test will fail however because the mocked TryParse has not been configured correctly, for example specifying that the method should return true.

The following modified test attempts to fix this:

[Fact]
public void ReturnHex_Fail_NoRefValueSetup()
{
var mockParser = new Mock<IParser>();
mockParser.Setup(x => x.TryParse(It.IsAny<string>(), ref It.Ref<int>.IsAny))
.Returns(true);

var sut = new Thing(mockParser.Object);

var result = sut.ConvertStringIntToHex("255");

Assert.Equal("FF", result); // Fails, actual result == 0
}


In the preceding code, the return value is being set, but nowhere is the ref int output “return value” being configured.

In the following test the Callback method is used to set the ref value. To be able to do this, a delegate must first be defined that matches the signature of the mocked method that contains the ref parameter. Once this delegate is defined it can be used in the Callback method as the following code demonstrates:

// Define a delegate that can be used to set the ref value in the mocked TryParse method
delegate void MockTryParseCallback(string number, ref int output);

[Fact]
public void ReturnHex()
{
var mockParser = new Mock<IParser>();
mockParser.Setup(x => x.TryParse("255", ref It.Ref<int>.IsAny)) // When the TryParse method is called with 255
.Callback(new MockTryParseCallback((string s, ref int output) => output = 255)) // Execute callback delegate and set the ref value
.Returns(true); // Return true as the result of the TryParse method

var sut = new Thing(mockParser.Object);

var result = sut.ConvertStringIntToHex("255");

Assert.Equal("FF", result);
}


If you’ve never used Moq or want to learn more about it check out the official Moq quickstart  or head over to my Pluralsight course.

SHARE:

It can be difficult  to write unit tests for code that accesses the file system.

It’s possible to write integration tests that read in an actual file from the file system, do some processing, and check the resultant output file (or result) for correctness. There are a number of potential problems with these types of integration tests including the potential for them to more run slowly (real IO access overheads), additional test file management/setup code, etc. (this does not mean that some integration tests wouldn’t be useful however).

The System.IO.Abstractions NuGet package can help to make file access code more testable. This package provides a layer of abstraction over the file system that is API-compatible with existing code.

Take the following code as an example:

using System.IO;
namespace ConsoleApp1
{
public class FileProcessorNotTestable
{
public void ConvertFirstLineToUpper(string inputFilePath)
{
string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");

using (StreamWriter outputWriter = File.CreateText(outputFilePath))
{
bool isFirstLine = true;

{

if (isFirstLine)
{
line = line.ToUpperInvariant();
isFirstLine = false;
}

outputWriter.WriteLine(line);
}
}
}
}
}


The preceding code opens a text file, and writes it to a new output file, but with the first line converted to uppercase.

This class is not easy to unit test however, it is tightly coupled to the physical file system with the calls to File.OpenText and File.CreateText.

Once the System.IO.Abstractions NuGet package is installed, the class can be refactored as follows:

using System.IO;
using System.IO.Abstractions;

namespace ConsoleApp1
{
public class FileProcessorTestable
{
private readonly IFileSystem _fileSystem;

public FileProcessorTestable() : this (new FileSystem()) {}

public FileProcessorTestable(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}

public void ConvertFirstLineToUpper(string inputFilePath)
{
string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");

using (StreamWriter outputWriter = _fileSystem.File.CreateText(outputFilePath))
{
bool isFirstLine = true;

{

if (isFirstLine)
{
line = line.ToUpperInvariant();
isFirstLine = false;
}

outputWriter.WriteLine(line);
}
}
}
}
}



The key things to notice in the preceding code is the ability to pass in an IFileSystem as a constructor parameter. The calls to File.OpenText and File.CreateText are now redirected to _fileSystem.File.OpenText and _fileSystem.File.CreateText  respectively.

If the parameterless constructor is used (e.g. in production at runtime) an instance of FileSystem will be used, however at test time, a mock IFileSystem can be supplied.

Handily, the System.IO.Abstractions.TestingHelpers NuGet package provides a pre-built mock file system that can be used in unit tests, as the following simple test demonstrates:

using System.IO.Abstractions.TestingHelpers;
using Xunit;

namespace XUnitTestProject1
{
public class FileProcessorTestableShould
{
[Fact]
public void ConvertFirstLine()
{
var mockFileSystem = new MockFileSystem();

var mockInputFile = new MockFileData("line1\nline2\nline3");

var sut = new FileProcessorTestable(mockFileSystem);
sut.ConvertFirstLineToUpper(@"C:\temp\in.txt");

MockFileData mockOutputFile = mockFileSystem.GetFile(@"C:\temp\in.out.txt");

string[] outputLines = mockOutputFile.TextContents.SplitLines();

Assert.Equal("LINE1", outputLines[0]);
Assert.Equal("line2", outputLines[1]);
Assert.Equal("line3", outputLines[2]);
}
}
}


To see this in action or to learn more about file access, check out my Working with Files and Streams in C# Pluralsight course.

SHARE: