2

私が開発したものに興味があります。これは、特定の DataTable を CSV ファイルにエクスポートし、それを応答に追加するカスタム FileResult です。

これが正しいテスト方法であるかどうかを知りたいだけです:

これが私のカスタム 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 を呼び出して、応答のコンテンツ タイプをチェックします。

これは正しいテスト方法ですか?他に良いテスト方法があれば教えてください。

敬具、

4

1 に答える 1

0

単体テストで行っていることと、SUT の動作が正しいことを確認する方法。継承を使用してテスト可能なバージョンを作成することを決定した手法も優れています。これを「抽出とオーバーライド」と呼びます。テストとテスト可能な sut (テスト対象のシステム) に簡単な変更を加えます。

a. 名前を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」のみを確認しているので、名前をより明確にします。このようにして、あなたの意図は非常に明確になります。ただし、複数の検証がある場合は、名前で十分でした。

ConvertToDataTable は必須ではありません。テストはできるだけ単純にしてください。テストに合格するために必要な最小量を使用してください。

一般的なコメント (私が片付けます) を除けば、他のすべてが目的に合っているようです。

于 2013-11-06T10:36:25.030 に答える