I'm curious about a thing that I developed. I've written a custom ActionMethod, which is a custom FileResult that will export a given DataTable to a CSV file and append it to the response.
I just want to know if this is the correct way of testing:
Here's my custom ActionResult:
/// <summary>
/// Represents an ActionResult that represents a CSV file.
/// </summary>
public class CsvActionResult : FileResult
{
#region Properties
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
public CsvActionResult(DataTable data)
: this(data, string.Format("Export_{0}.csv", DateTime.Now.ToShortTimeString()), true, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
public CsvActionResult(DataTable data, string name)
: this(data, name, true, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, string usedDelimeter)
: this(data, name, true, Encoding.Default, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
public CsvActionResult(DataTable data, string name, bool addRowHeaders)
: this(data, name, addRowHeaders, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, bool addRowHeaders, string usedDelimeter)
: this(data, name, addRowHeaders, Encoding.Default, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
public CsvActionResult(DataTable data, string name, Encoding usedEncoding)
: this(data, name, true, usedEncoding, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, Encoding usedEncoding, string usedDelimeter)
: this(data, name, true, usedEncoding, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
public CsvActionResult(DataTable data, bool addRowHeaders)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, Encoding.Default, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, bool addRowHeaders, string usedDelimeter)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, Encoding.Default, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedEncoding">The encoding to use.</param>
public CsvActionResult(DataTable data, bool addRowHeaders, Encoding usedEncoding)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, usedEncoding, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, bool addRowHeaders, Encoding usedEncoding, string usedDelimeter)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), addRowHeaders, usedEncoding, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
public CsvActionResult(DataTable data, Encoding usedEncoding)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), true, usedEncoding, ";")
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, Encoding usedEncoding, string usedDelimeter)
: this(data, string.Format("Export_{0}", DateTime.Now.ToShortTimeString()), true, usedEncoding, usedDelimeter)
{ }
/// <summary>
/// Creates a new instance of the <see cref="CsvActionResult"/> class.
/// </summary>
/// <param name="data">The data which needs to be exported to a CSV file.</param>
/// <param name="name">The filename of the returned file.</param>
/// <param name="addRowHeaders">A boolean that indicates wether to include row headers in the CSV file or not.</param>
/// <param name="usedEncoding">The encoding to use.</param>
/// <param name="usedDelimeter">The delimeter to use as a seperator.</param>
public CsvActionResult(DataTable data, string name, bool addRowHeaders, Encoding usedEncoding, string usedDelimeter)
: base("text/csv")
{
this.dataTable = data;
this.filename = name;
this.includeRowHeader = addRowHeaders;
this.encoding = usedEncoding;
this.delimeter = usedDelimeter;
}
/// <summary>
/// The datatable that needs to be exported to a Csv file.
/// </summary>
private readonly DataTable dataTable;
/// <summary>
/// The filename that the returned file should have.
/// </summary>
private readonly string filename;
/// <summary>
/// A boolean that indicates wether to include the row header in the CSV file or not.
/// </summary>
private readonly bool includeRowHeader;
/// <summary>
/// The encoding to use.
/// </summary>
private readonly Encoding encoding;
/// <summary>
/// The delimeter to use as a seperator.
/// </summary>
private readonly string delimeter;
#endregion Properties
#region Methods
/// <summary>
/// Start writing the file.
/// </summary>
/// <param name="response">The response object.</param>
protected override void WriteFile(HttpResponseBase response)
{
//// Add the header and the content type required for this view.
//response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
//response.ContentType = base.ContentType;
// Add the header and the content type required for this view.
string format = string.Format("attachment; filename={0}", "somefile.csv");
response.AddHeader("Content-Disposition", format);
response.ContentType = "text/csv"; //if you use base.ContentType,
//please make sure this return the "text/csv" during test execution.
// Gets the current output stream.
var outputStream = response.OutputStream;
// Create a new memorystream.
using (var memoryStream = new MemoryStream())
{
WriteDataTable(memoryStream);
outputStream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}
#endregion Methods
#region Helper Methods
/// <summary>
/// Writes a datatable to a given stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
private void WriteDataTable(Stream stream)
{
var streamWriter = new StreamWriter(stream, encoding);
// Write the header only if it's indicated to write.
if (includeRowHeader)
{ WriteHeaderLine(streamWriter); }
// Move to the next line.
streamWriter.WriteLine();
WriteDataLines(streamWriter);
streamWriter.Flush();
}
/// <summary>
/// Writes the header to a given stream.
/// </summary>
/// <param name="streamWriter">The stream to write to.</param>
private void WriteHeaderLine(StreamWriter streamWriter)
{
foreach (DataColumn dataColumn in dataTable.Columns)
{
WriteValue(streamWriter, dataColumn.ColumnName);
}
}
/// <summary>
/// Writes the data lines to a given stream.
/// </summary>
/// <param name="streamWriter"><The stream to write to./param>
private void WriteDataLines(StreamWriter streamWriter)
{
// Loop over all the rows.
foreach (DataRow dataRow in dataTable.Rows)
{
// Loop over all the colums and write the value.
foreach (DataColumn dataColumn in dataTable.Columns)
{ WriteValue(streamWriter, dataRow[dataColumn.ColumnName].ToString()); }
streamWriter.WriteLine();
}
}
/// <summary>
/// Write a specific value to a given stream.
/// </summary>
/// <param name="writer">The stream to write to.</param>
/// <param name="value">The value to write.</param>
private void WriteValue(StreamWriter writer, String value)
{
writer.Write(value);
writer.Write(delimeter);
}
#endregion Helper Methods
}
The start method of this class is WriteFile, but since this is a protected method, I've created a class in my unit test project which allows me to access this:
public class CsvActionResultTestClass : CsvActionResult
{
public CsvActionResultTestClass(DataTable dt)
: base(dt)
{
}
public new void WriteFile(HttpResponseBase response)
{ base.WriteFile(response); }
}
Basiclly, I'm creating a class that inherits from the CsvActionResult and that allows me to execute the WriteFile method.
In my unit test itself, I do execute the following code:
[TestMethod]
public void CsvActionResultController_ExportToCSV_VerifyResponsePropertiesAreSetWithExpectedValues()
{
// Initialize the test.
List<Person> persons = new List<Person>();
persons.Add(new Person() { Name = "P1_Name", Firstname = "P1_Firstname", Age = 0 });
persons.Add(new Person() { Name = "P2_Name", Firstname = "P2_Firstname" });
// Execute the test.
DataTable dtPersons = persons.ConvertToDatatable<Person>();
var httpResponseBaseMock = new Mock<HttpResponseBase>();
//This would return a fake Output stream to you SUT
httpResponseBaseMock.Setup(x => x.OutputStream).Returns(new Mock<Stream>().Object);
//the rest of response setup
CsvActionResultTestClass sut = new CsvActionResultTestClass(dtPersons);
sut.WriteFile(httpResponseBaseMock.Object);
//sut
httpResponseBaseMock.VerifySet(response => response.ContentType = "text/csv");
}
This method creates a DataTable and mock the HttpResponseBase.
Then I'm calling the method WriteFile and checks the content type of the response.
Is this the correct way of testing? If there are other, better ways of testing, please tell me.
Kind regards,