gpt4 book ai didi

c# - 这是测试 ActionResult 的正确方法吗

转载 作者:太空宇宙 更新时间:2023-11-03 16:05:05 26 4
gpt4 key购买 nike

我对我开发的东西很好奇。我编写了一个自定义 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,但由于这是一个 protected 方法,我在我的单元测试项目中创建了一个类,它允许我访问它:

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 并检查响应的内容类型。

这是正确的测试方法吗?如果还有其他更好的测试方法,请告诉我。

亲切的问候,

最佳答案

您在单元测试中做了什么,以及您如何验证 SUT 的行为是否正确。您决定使用继承来创建可测试版本的技术也很好。这称为“Extract and Override”。我会对您的测试和可测试的 sut(被测系统)进行简单的更改。

一个。我会将名称更改为 TestableCsvActionResult

 public class TestableCsvActionResult : CsvActionResult
{
public TestableCsvActionResult(DataTable dt)
: base(dt)
{
}

public new void WriteFile(HttpResponseBase response)
{ base.WriteFile(response); }
}

这样你提供一个可测试的版本就更有意义了。而且它不是假的。

单元测试

    [TestMethod]
public void CsvActionResultController_ExportToCSV_VerifyResponseContentTypeIsTextCsv()
{
// Arrange
var httpResponseBaseMock = new Mock<HttpResponseBase>();
httpResponseBaseMock.Setup(x => x.OutputStream).Returns(new Mock<Stream>().Object);
var sut = new CsvActionResultTestClass(new DataTable());

//Act
sut.WriteFile(httpResponseBaseStub.Object);

//Verify
httpResponseBaseMock.VerifySet(response => response.ContentType = "text/csv");
}

您的测试方法名称很好,而且可读。由于您只是在验证“text/csv”,因此我会更明确地说明名称。这样就很清楚你的意图了。但是,如果您有多个验证,那么您拥有的名称就足够了。

ConverToDataTable 不是必需的。使测试尽可能简单。使用使您的测试通过所需的最小量。

除了一般评论(由我整理)之外,其他一切似乎都符合目的。

关于c# - 这是测试 ActionResult 的正确方法吗,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19807399/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com