C# Unit Testing a Custom FileResult That Exports Data into a CSV file Using Streaming in a .NET Core 3.1 MVC App
Kim Diep
Posted on May 29, 2020
Introduction
In the past two months at work, I was tasked with learning C#, as well as creating a web app using the .NET Core 3.1 MVC framework. I wanted to document the most interesting concepts in a series of blog posts.
In my last blog, I demonstrated how to create a custom FileResult to export data into a CSV file using streaming in a .NET Core 3.1 MVC web app. In this follow on blog post, I will show you how to unit test the custom FileResult and the controller which produces this FileResult.
Again, the actual code was more complex; this blog was my attempt to abstract the core concepts into a simple web app using Pusheen the Cat as a fun example!
Unit Testing Custom FileResult With Streaming in .NET Core 3.1
In my last blog, I had a custom FileResult called PusheenCsvResult. To set the scene for unit testing, I used the NUnit testing framework, along with the FluentAssertions and FluentAssertions.AspNetCore.Mvc libraries, which provided a clear way to communicate what I was asserting in my test (i.e. what was the expected result). I applied the Arrange, Act, Assert structure for this and am still learning the best way to do it!
P.S. Would highly recommend the book Agile Technical Practices Distilled: A learning journey in technical practices and principles of software design.
What was the goal?
Let's start off with the goal! When we're working with unit testing, it's helpful to define what it is we're checking for. In this unit testing situation, I wanted a way to check that the PusheenCsvResult's method ExecuteResultAsync was streaming (writing) the response correctly to the HttpContext's response body.
How did I go about doing it?
Knowing this, I followed some tips in the Agile Technical Practices Distilled book to start from the assertion and work backwards (Assert, Act , Arrange). I didn't just magically know what I needed; it took some time to get there.
Setting it up
I created a [TestFixture] for testing PusheenCsvResult and within the [SetUp], I defined a _httpContext, _fileDownloadName and made a fake _fakeActionContext object.
The reason I did this was because the PusheenCsvResult's method ExecuteResultAsync, took a parameter which was of type ActionContext.
Let's recap on my last blog for a second, the job of ExecuteResultAsync is using StreamWriter to write to the response body of the HttpContext of the ActionContext and the stream sits in between the application and in this case the response body. The data is written to the stream (response body) and then from the stream, it then results in the CSV file being produced.
In the case of the unit test scope, I wanted to create a _fakeActionContext object as an instance of ActionContext and in it's constructor, I set the HttpContext as the _httpContext I defined earlier in my test [SetUp]. This enabled the ability to check what was written to the response body of that _httpContext.
#Code omitted for brevity
[TestFixture]
public class PusheenCsvResultShould
{
private PusheenCsvResult _pusheenCsvResult;
private string _fileDownloadName;
private string _expectedResponseText;
private DefaultHttpContext _httpContext;
private ActionContext _fakeActionContext;
[SetUp]
public void Setup()
{
_httpContext = new DefaultHttpContext();
_fileDownloadName = "pusheen.csv";
_fakeActionContext = new ActionContext()
{
HttpContext = _httpContext
};
}
[Test]
public async Task GivenActionContext_ExecuteResultAsync_ShouldWriteLineToHttpResponseBody()
{
//Arrange
var data = new List<Pusheen>()
{
new Pusheen() { Id = 1, Name = "Pusheen", FavouriteFood = "Ice cream", SuperPower = "Baking delicious cookies" },
new Pusheen() { Id = 2, Name = "Pusheenosaurus", FavouriteFood = "Leaves", SuperPower = "Roarrrrr!" },
new Pusheen() { Id = 3, Name = "Pusheenicorn", FavouriteFood = "Butterfly muffins", SuperPower = "Making rainbow poop" }
}.AsQueryable();
PusheenCsvResult _pusheenCsvResult = new PusheenCsvResult(data, _fileDownloadName);
_expectedResponseText = System.IO.File.ReadAllText(TestContext.CurrentContext.TestDirectory + @"/TestData/expectedCsv.txt");
var memoryStream = new MemoryStream();
_httpContext.Response.Body = memoryStream;
//Act
await _pusheenCsvResult.ExecuteResultAsync(_fakeActionContext);
var streamText = System.Text.Encoding.Default.GetString(memoryStream.ToArray());
//Assert
streamText.Should().Be(_expectedResponseText);
}
}
Let's hop over to the test
For the [Test] itself, I checked that given an ActionContext, the method ExecuteResultAsync should WriteLine to the HttpContext response body.
I needed a PusheenCsvResult for my test, and that took 2 parameters for it's constructor.
- data (as type IQueryable)
- fileDownloadName (as type String)
I already defined fileDownloadName earlier in the [SetUp], so the next step was to make some data for the test scenario. In this case, a new List as .AsQueryable() was created and I passed this into PusheenCsvResult's constructor.
Based on this information, I wanted to make a file containing the expected text I would expect to see as _expectedResponseText. In my assertion, I checked that the text I got from the stream should match the _expectedResponseText for the test to pass.
Now, this was the tricky bit - how to deal with closed streams?
When I was testing this, I didn't know what was wrong, as the test kept saying that it couldn't access a closed stream. Since I defined the StreamWriter within a using block; the stream will be closed once it's done its job. This was not a bad thing and is something I recommend doing in your implementation; but it meant I needed another way to access what was written to the stream for the purpose of the unit testing (in this case, the stream was the HttpContext's response body itself).
I added some comments on the code snippet below to describe what was going on.
// I create a new Memory Stream and set that stream as the Response Body of the _httpContext I'm using in my unit test scope
var memoryStream = new MemoryStream();
_httpContext.Response.Body = memoryStream;
//Act
// I await and pass the _fakeActionContext to my ExecuteResultAsync method. Reminder that I pointed the HttpContext of ActionContext to the _httpContext I made for testing
await _pusheenCsvResult.ExecuteResultAsync(_fakeActionContext);
//I need to make sure that I capture the contents of the memoryStream and store it against the variable streamText which I can access later in my assertion
var streamText = System.Text.Encoding.Default.GetString(memoryStream.ToArray());
Unit testing the Controller
The controller was a bit more straightforward. I used Moq to mock the PusheenService and its method GetAllPusheens() to return some data.
_mockPusheenService.Setup(p => p.GetAllPusheens()).Returns(data);
Here, I tested that ExportCsv on the PusheenController returned the result of type PusheenCsvResult and that the fileDownloadName and contentType were correct.
#The rest of the code has been omitted for brevity! :)
namespace PusheenCustomExportCsv.Tests.Controllers
{
[TestFixture]
public class PusheenControllerShould
{
private PusheenController _controller;
private Mock<IPusheenService> _mockPusheenService;
private Mock<IConfiguration> _mockConfig;
private DbContextOptions<PusheenCustomExportCsvContext> _testDbOptions;
private PusheenCustomExportCsvContext _testDbContext;
[SetUp]
public void Setup()
{
_mockPusheenService = new Mock<IPusheenService>();
_controller = new PusheenController(_mockPusheenService.Object);
}
[Test]
public void ExportCsv_Returns_CsvResult()
{
//Arrange
var data = new List<Pusheen>()
{
new Pusheen() { Id = 1, Name = "Pusheen", FavouriteFood = "Ice cream", SuperPower = "Baking delicious cookies" },
new Pusheen() { Id = 2, Name = "Pusheenosaurus", FavouriteFood = "Leaves", SuperPower = "Roarrrrr!" },
new Pusheen() { Id = 3, Name = "Pusheenicorn", FavouriteFood = "Butterfly muffins", SuperPower = "Making rainbow poop" }
}.AsQueryable();
_mockPusheenService.Setup(p => p.GetAllPusheens()).Returns(data);
//Act
var result = _controller.ExportCsv();
//Assert
result.Should().BeOfType(typeof(PusheenCsvResult));
result.FileDownloadName.Should().Be("pusheen.csv");
result.ContentType.Should().Be("text/csv");
}
}
}
Final Thoughts
I hope you find this post useful and being part of my coding journey! Thank you for reading my blog! :)
Link to Github Repo
You can find the link to my Github repo with the simple web app example containing the custom fileresult and test project here.
Posted on May 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024