我对我开发的东西很好奇。我编写了一个自定义 ActionMethod,它是一个自定义 FileResult,它将给定 DataTable 导出到 CSV 文件并将其附加到响应中。
我只想知道这是否是正确的测试方法:
这是我的自定义 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
}
这个类的启动方法是 WriteFile,但由于这是一个受保护的方法,我在我的单元测试项目中创建了一个类,它允许我访问它:
public class CsvActionResultTestClass : CsvActionResult
{
public CsvActionResultTestClass(DataTable dt)
: base(dt)
{
}
public new void WriteFile(HttpResponseBase response)
{ base.WriteFile(response); }
}
基本上,我正在创建一个继承自 CsvActionResult 并允许我执行 WriteFile 方法的类。
在我的单元测试本身中,我确实执行了以下代码:
[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");
}
此方法创建一个 DataTable 并模拟 HttpResponseBase。
然后我调用方法 WriteFile 并检查响应的内容类型。
这是正确的测试方法吗?如果有其他更好的测试方法,请告诉我。
亲切的问候,