The Problem
In an N-tier application, you keep your logic in a business logic tier, typically a different VS project that can be used from a website, a windows service, or desktop application, and that should be valid to writ unit tests against on its own.
But how about if your requirements say that you need to to upload some file for the business logic to work? Think of a scanned image (signed contract maybe?) or just a comma separated value file containing some emails. Typically the business logic tier will be the place to to handle this, but how can you send the uploaded file to it? You can get the file as "HttpPostedFile" from the "Request.Files" collection or the file upload control itself, but, to receive it in the business logic, project, the easiest way is to add reference to "System.Web" dll and accept the type "HttpPostedFile" as method argument or so, and when in need to save the file physically, you call the "SaveAs" method of "HttpPostedFile" … So simple right ?
But how about unit testing? "HttpPostedFile" has no public constructor and is sealed class (you cannot inherit from). How will you write a unit test for a method that accepts "HttpPostedFile" as argument to work? You’d go finding a way to mock that or just forget about testing that particular method!!
The Simple Solution
Well, it’s more simple than you think. An "HttpPostedFile" has a property called "InputStream", which is of type "System.IO.Stream". Hey, that has nothing to do with "system.Web" :). You can create your own FileStream or whatever other stream in the unit test, and then only worry about other areas you want to test related to the method in your business logic.
Because I know dealing with streams is quite ugly (at least it is to me!), I wrote an example that will walk you though a sample usage of that property. I have the complete example VS solution as attached file at the end of the example code below.
Example
The Business Logic
In a real world example, the business class will have various parameters including the file, but in this example, I have made my business class (I call it "FileLogic") just worry about the file. It has a method that treats the stream coming to it as containing just text, reads that text and returns it. Note the type of the only method parameter
/// <summary>
/// This is the simplest method ever. Read the stream whether from web upload or whatever
/// As the name implies, it treats the stream contents as text.
/// </summary>
public string ReadTextFile(Stream inputStream)
{
using (var reaader = new System.IO.StreamReader(inputStream))
{
return reaader.ReadToEnd();
}
}
Another example of a method in the business logic project is one that actually saves the file. Instead of just calling "SaveAs(filepath)", you need to copy the stream yourself. Usually there’s more than one way to do so, the problem in most of the ways is determining the size of the stream. An "HttpPostedFile" knows the size of the file, but as I’m not getting the "HttpPostedFile" (and it wouldn’t sound great if I requested the file size as a method parameter!), I dealt as if I do not know the actual file size. See the code for this method:
/// <summary>
/// Writes the stream context that comes to it whether from web or whatever!
/// and saves it to the path set by property called "SavingPath"
///
/// I meant to not make the save path a method property because
/// in real situation that value would come most likely from
/// configuration, DB or some other code logic
/// </summary>
public void WriteBinaryFile(Stream inputStream)
{
using (var reader = new BinaryReader(inputStream))
{
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int count = 0;
int offset = 0;
using (var writingStream =
new System.IO.FileStream(this.SavingPath, System.IO.FileMode.OpenOrCreate))
{
while ((count = reader.Read(buffer, offset, buffer.Length)) > 0)
{
writingStream.Write(buffer, offset, count);
}
writingStream.Flush();
writingStream.Close();
}
reader.Close();
}
}
The UI (Website)
Before we dig into the unit test sample, let’s see how our page code will look when dealing with this business logic class.
Consider a page with controls like these:
<%--
Note: I used the FileUpload control of .NET 2.0 and above,
but I could also use something like:
<input type="file" runat="server" id="inFileTest" />
--%>
<asp:FileUpload runat="server" ID="upldTestFile" />
<%-- Used a text file read operation as a simple exsample --%>
<asp:Button runat="server" ID="btnReadFile" Text="Read text file in the page"
onclick="btnReadFile_Click" />
<%-- Note that even text files can be treated as binary files :) --%>
<asp:Button runat="server" ID="btnWriteFile" Text="Write Copy of the file (any type)"
onclick="btnWriteFile_Click" />
<br />
<%-- I'll show the read happened by referencing to it --%>
<asp:Literal runat="server" ID="litReadFileResult" />
<%-- I'll show the file was written by linking to it.
In real world, you may want to encapsulate this in an HTTPHandler
not expose the file itself --%>
<asp:HyperLink runat="server" ID="lnkWriteFileResult"
Text="Click here to get the saved file" />
The code behind for such page with our custom logic class (in a sample manner still), would look like this:
/// <summary>
/// Created for the "ReadTextFile" method
/// </summary>
protected void btnReadFile_Click(object sender, EventArgs e)
{
if (!upldTestFile.HasFile) { /* Call whatever error msg method and, */ return; }
var fileLogic = new FileLogic();
litReadFileResult.Text =
"The text file contents are: "
+ fileLogic.ReadTextFile(upldTestFile.PostedFile.InputStream);
litReadFileResult.Visible = true;
}
/// <summary>
/// Created for the "WriteBinaryFile" method
/// </summary>
protected void btnWriteFile_Click(object sender, EventArgs e)
{
if (!upldTestFile.HasFile) { /* Call whatever error msg method and, */ return; }
//Save the file to "sample.text" under the website root (referenced as "~/"
string relativeSavePath = "~/" + upldTestFile.FileName;
var fileLogic = new FileLogic();
// Converting path to phyical then setting it in the business logic class
// The physical path typically does not yet exist, as we'll create new file to it
fileLogic.SavingPath = Server.MapPath(relativeSavePath);
fileLogic.WriteBinaryFile(upldTestFile.PostedFile.InputStream);
lnkWriteFileResult.NavigateUrl = relativeSavePath;
lnkWriteFileResult.Visible = true;
}
I’d say that this part is not much different than it’d be if we were passing the "HttpPostedFile" object complete.
The Unit Test
So, that’s what we have been hassling, complicating code, and dealing directly with streams for its sake!
As our application will not necessarily be a file sharing application, we’ll be in need for the file as part of a bigger operation. A unit test will not be only concerned with the file upload task, but also the rest parts of the business operation our method intends to do. However, in this sample, I’ve decided to make it just test the file uploading functionality, and included really simple ways in the unit test.
Instead of the file upload, I used a physical file that I get its path from a method called "GetTestFilename" (I could have used configuration file, static property or else for the file stream, or used any other kind of stream still).
See how the test methods look in this example (using NUnit):
/// <summary>
/// Checks method "ReadTextFile" to see whether it gets the same text as sent file
/// </summary>
[Test]
public void TestReadTextFile()
{
var fileLogic = new FileLogic();
var sampleFileInfo = new System.IO.FileInfo(GetTestFilename());
string correctResult = sampleFileInfo.OpenText().ReadToEnd();
string testResult = fileLogic.ReadTextFile(sampleFileInfo.OpenRead());
Assert.AreEqual(correctResult, testResult);
}
/// <summary>
/// Checks whether method "WriteBinaryFile" creates the file
/// </summary>
[Test]
public void TestWriteBinaryFile_FileExists()
{
var fileLogic = new FileLogic();
string newFilename = System.IO.Path.GetTempFileName();
fileLogic.SavingPath = newFilename;
var sampleFileInfo = new System.IO.FileInfo(GetTestFilename());
fileLogic.WriteBinaryFile(sampleFileInfo.OpenRead());
Assert.IsTrue(System.IO.File.Exists(newFilename));
}
//There should be one more method for checking the file contents are the same
// Skipped for sample!
The Complete VS 2008 Solution
I’ve attached a complete VS Solution of this example with few more comments than here so that you can see it more closely. This is a ZIP compressed VS 2008 solution.
Meligy.Samples.TestableWebFileUpload.zip
Background of this Article (Totally Useless. Serious Warning)
Almost a year and half ago (Just a month after joining SilverKey), I was asked to build a file sharing area as a module of community site for one of our biggest clients. This involved much extension of an existing file upload business logic component that was written for internal usage in other systems for the same client to allow logical folders and large file upload with very high frequency at certain times of the year. A month or more ago (just before my exam vacation), another developer was asked to build a new file sharing component for another client but for certain business field not community stuff, and to make it generically reusable in any other systems (as our original component was quite tied to other rules in the general infrastructure of all the systems for the same big client). I was involved in design meetings with the other developer of course for what insights and snippets could be taken from the original component and my own extension as well.
After I got back from vacation (last week or so). I heard some notes from our tester (who also does his own unit tests whether or not the developer does :D), about the business logic not being "testable" because the business logic was passed "HttpPostedFile". The same applied to the old component actually.
So, today I wasn’t exactly in the best mood, and a friend was asking me why I wasn’t blogging for long (I said because I was more into reading than writing nowadays), so, I thought why not spend the night in refreshing my mind by writing some really basic example to show how to overcome that simple issue that I believe most people just do not realize!
Disclaimer
Again, this is something I wrote as a fix for some bad mood and was intended just to be a very simple example. I know 1000% that there’re other better ways to write each and every part of this code, and many issues was not covered. The desired parts to show here were the layering (including unit tests) and different ways to deal with streams. To be clear, I’ll make sure to support questions and such about it, but I’m not sure whether you can consider this a promise !!
Related posts:
- Using EQUATEC FREE .NET Profiler with ASP.NET Applications
This was originally an email I sent to .NET team... - GridView DataBinding Events
This is a well commented example for a GridView with... - Prevent ASP.NET Validators from Massively Increasing Page Size
This is problematic with ASP.NET AJAX. The main Script Components...
CV Download (.docx)
Google Reader Shared Items
LinkedIn Recommendations
Twitter Updates