【问题标题】:Individual unit tests for each output of a method方法的每个输出的单独单元测试
【发布时间】:2020-01-02 03:38:05
【问题描述】:

我有一个方法,它接受一个文件作为输入,然后根据这个文件返回 N 个输出。

我想通过以下方式测试这个方法:假设我们有 M 个文件要测试。对于每个文件,我想在测试程序(或单独的文件)中添加一行,由文件路径和 N 个预期输出组成。这些数据应该会产生 N*M 个单独的测试,每对文件和预期输出一个。

有没有好的方法来实现这一点?我希望每次测试运行时对每个文件的解析不超过一次。

下面是一个做我想要的例子。如您所见,我必须为每个文件添加单独的测试类。我希望找到一个解决方案,我可以只添加带有测试数据的行(例如testData.Add(("thirdfile", 4), (348, 312));)来测试新文件。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }

    public static class FileParser
    {
        private static int n = 0;

        public static void Init(int parameter)
        {
            n = parameter;
        }

        public static (int output1, int output2) ParseFile(string filename)
        {
            return (filename[0] * n, filename[1] * n);
        }
    }

    public class Tests
    {
        private Dictionary<(string, int), (int, int)> testData;

        public Tests()
        {
            testData = new Dictionary<(string, int), (int, int)>();
            testData.Add(("somefile", 3), (345, 333));
            testData.Add(("anotherfile", 4), (291, 330));
            testData.Add(("thirdfile", 4), (348, 312));
        }

        public void TestOutput1((int, int) result, string filename, int parameter)
        {
            Assert.AreEqual(testData[(filename, parameter)].Item1, result.Item1);
        }

        public void TestOutput2((int, int) result, string filename, int parameter)
        {
            Assert.AreEqual(testData[(filename, parameter)].Item2, result.Item2);
        }
    }

    [TestClass]
    public class Somefile
    {
        protected static (int, int) fileParseResult;

        [ClassInitialize]
        public static void ClassInit(TestContext context)
        {
            FileParser.Init(3);
            fileParseResult = FileParser.ParseFile("somefile");
        }

        [TestMethod]
        public void SomefileOutput1() { var tests = new Tests(); tests.TestOutput1(fileParseResult, "somefile", 3); }
        [TestMethod]
        public void SomefileOutput2() { var tests = new Tests(); tests.TestOutput2(fileParseResult, "somefile", 3); }
    }

    [TestClass]
    public class Anotherfile
    {
        protected static (int, int) fileParseResult;

        [ClassInitialize]
        public static void ClassInit(TestContext context)
        {
            FileParser.Init(3);
            fileParseResult = FileParser.ParseFile("anotherfile");
        }

        [TestMethod]
        public void AnotherfileOutput1() { var tests = new Tests(); tests.TestOutput1(fileParseResult, "anotherfile", 4); }
        [TestMethod]
        public void AnotherfileOutput2() { var tests = new Tests(); tests.TestOutput2(fileParseResult, "anotherfile", 4); }
    }

    [TestClass]
    public class Thirdfile
    {
        protected static (int, int) fileParseResult;

        [ClassInitialize]
        public static void ClassInit(TestContext context)
        {
            FileParser.Init(3);
            fileParseResult = FileParser.ParseFile("thirdfile");
        }

        [TestMethod]
        public void ThirdfileOutput1() { var tests = new Tests(); tests.TestOutput1(fileParseResult, "thirdfile", 4); }
        [TestMethod]
        public void ThirdfileOutput2() { var tests = new Tests(); tests.TestOutput2(fileParseResult, "thirdfile", 4); }
    }
}

【问题讨论】:

  • 您标记了xunit.net,但[TestMethod] 不是来自Microsoft 的 测试框架吗?我认为 xUnit 有[Fact] 属性? - 具体的测试框架可能对于可能的实现至关重要。
  • @Jeroen 是的,没错。我希望能够使用 xUnit,但我对两者都持开放态度。
  • “如果涉及文件系统,测试就不是单元测试”artima.com/weblogs/viewpost.jsp?thread=126923 但是您可以使用github.com/System-IO-Abstractions/System.IO.Abstractions轻松模拟文件系统
  • 为什么不用T4模板生成单元测试代码呢?所有的文件探测和解析魔法都在生成器中,单元测试最终变得干净简单。
  • @dfhwze 我没听说过。我会调查的。

标签: c# unit-testing mstest xunit xunit.net


【解决方案1】:

如果您对 MSTest 或 xUnit 以外的项目开放,您可以查看 Nuclear.Test

创建一个数据驱动的测试方法,负责解析同时检查两个结果项。

[TestMethod]
[TestParamters("someFile", 3, (345, 333))]
[TestParamters("anotherfile", 4, (291, 330))]
[TestParamters("thirdfile", 4, (348, 312))]
void TestFile(String someFile, Int32 parameter, (Int32, Int32) expected) {

    (Int32, Int32) result = null;
    
    Test.Note("Parsing '" + someFile + "'");
    Test.IfNot.Action.ThrowsException(() => FileParser.Init(parameter), out Exception ex);
    Test.IfNot.Action.ThrowsException(() => result = FileParser.ParseFile(someFile), out ex);
    Test.IfNot.Object.IsNull(result);
    
    Test.Note("Checking results for '" + someFile + "'");
    Test.If.Value.IsEqual(result.Item1, expected.Item1);
    Test.If.Value.IsEqual(result.Item2, expected.Item2);
    
}

有关使用 Nuclear.Test 编写数据驱动测试的更多信息,请访问 here

请注意,目前这至少需要 .NETStandard 2.0。 我确实意识到您可能不会对不同的单元测试平台持开放态度,但是既然您确实说过您对 MSTest 或 xUnit 持开放态度,我想您还没有完全决定。

这种方法也适用于 MSTest 和 xUnit,但在这些情况下这会破坏 OAPT,而 Nuclear.Test 不受这些限制的影响。

请注意,由于我是用手机编写的,因此无法测试此代码。里面可能有错别字或错误。

更新:

这将是使用 MSTest 的合法方法:

[TestMethod]
[DataRow("someFile", 3, (345, 333))]
[DataRow("anotherfile", 4, (291, 330))]
[DataRow("thirdfile", 4, (348, 312))]
public void TestFileParser(String fileName, Int32 parameter, (Int32, Int32) expected) {

    FileParser.Init(parameter);
    var result = FileParser.ParseFile(fileName);
    Assert.AreEqual(result, expected);

}

原来 ValueTuple 实现了 IComparable 并且可以在一个断言中进行比较。

【讨论】:

  • 这看起来很有趣。我需要每个输出一个测试,如果这就是你的意思,我在 MSTest 或 xUnit 下无法使用这种方法。但是,我认为缺少 Visual Studio 集成使我无法使用 Nuclear.Test。
  • 如果您不太关心 oapt(我认为在这种情况下可以忽略),那么只需使用 xunit 使用这种方法。它将为您提供相同的灵活性,即每个测试用例添加一行代码,同时实施起来不会那么痛苦。它将是 3 个断言,您只需解析每个文件一次。
【解决方案2】:

使用 xUnit 和 System Linq

使用包含两个测试结果的 InlineData( 或 MemberData,如果您希望将其分开)涵盖了添加一行数据以运行多项检查的要求,但我不确定 p>

每个文件只能解析一次,而不是每次测试一次。

无需通过某种方式记录您在之前的测试运行中解析过的文件,并将其插入到 if() 语句中

public class ExampleTest
{
    [Theory]
    [InlineData ("somefile", 3, 332, 354)]
    [InlineData ("anotherfile", 3, 290, 337)]
    [InlineData ("thirdfile", 4, 310, 304)]
    public void FileParseOutputIsCorrect ( string fileName, int parameter, int resultA, int resultB )
    {
        //conditional check only necessary if you want to stop parsing in future test runs
        if ( !fileName.Parsed )
        {
            var fileParseResult = FileParser.ParseFile ( fileName, parameter );
            Assert.Equal ( fileParseResult[0], resultA );
            Assert.Equal ( fileParseResult[1], resultB );
        }
        else
        {
            Console.WriteLine ( $"Already parsed {fileName}" );
        }
    }
}

【讨论】:

    【解决方案3】:

    我同意@Andreas 保持单元测试简单的观点。因此,不建议阅读配置文件(配置单元测试功能的文件)。以下示例代码通过确保每个文件只被读取一次来扩展 @James Pusateri 的好答案。

        [TestClass]
        public class UnitTest1
        {
            // Use a static Lazy<T> instance to read your file just once. 
            // Replace <object> with your type. 
            // Use multiple of these Lazy variables by using a Dictionary<string, Lazy<YourResultType>>
            private static Lazy<object> fileParseResult = new Lazy<object>(() => FileParser.ParseFile("somefile"));
    
            [ClassCleanup]
            public static void ClassCleanup()
            {
                // in case you need to clean up something, do it here
                // fileParseResult.Value.Dispose() if applicable
                fileParseResult = null;
            }
    
            [TestMethod]
            [DataRow("somefile", 3, 345, 333)]
            [DataRow("anotherfile", 4, 291, 330)]
            // add additional DataRow-lines here as required
            public void OutputIsValid(string fileName, int parameter, int resultX, int resultY)
            {
                // make sure to only read 'fileParseResult.Value' and not change it.
                Assert.AreEqual(fileParseResult.Value, fileName);
            }
    
            // dummy implementation for testing this code. Use your implemenation instead.
            private class FileParser
            {
                internal static object ParseFile(string v) => v;
            }
        }
    

    MSTest 这是一份不错的备忘单https://www.automatetheplanet.com/mstest-cheat-sheet/

    xUnit 具有类似的方法,但使用不同的属性[Theory][InlineData]。此外,xUnit 具有更复杂(和复杂)的上下文共享可能性https://xunit.net/docs/shared-context。到目前为止,我一直设法以不需要这些高级上下文共享功能的方式简化测试场景。

    经验和个人意见 实际上,我不使用任何上下文共享进行单元测试。原因是重构。考虑添加另一个需要稍微不同的共享上下文的测试方法的需要。因此,您将继续更改共享上下文并无意中破坏了许多现有测试。此外,我避免读取单元测试中的任何文件,而是使用返回预定义和可预测结果的模拟。

    【讨论】:

    • 谢谢。一旦我可以访问 Visual Studio,我会更好地了解这一点。不过,我有两个问题。 anotherfile 在这段代码中解析在哪里?而当你说读取配置文件不是一个选项时,你的意思是不建议read the test data from a CSV file
    • 配置文件是可能的,但恕我直言,不建议这样做。 'anotherfile' 可以通过复制 Lazy 变量或使用列表或字典来添加。
    • 这里的目标是能够添加新的测试,只需将一行包含测试数据的代码添加到代码中,或者最好添加到外部文件中。这种方法可以实现吗?
    • 对不起,我在重复自己。 [DataRow("some-third-file", 1, 2, 3)] 的新代码行添加了一个新测试。恕我直言,单元测试应该是编码问题,而不是配置问题。编码时,您有编译器检查、IntelliSense 重构工具等。编写配置文件时,您什么都没有。
    【解决方案4】:

    按照@pwrigshihanomoronimos 的方法,有一种方法可以用更少的代码实现这一点。但是你很可能需要单元测试来确保测试正常工作,所以我建议不要这样做。

    我知道,编写这样的测试可能非常乏味,但是对于单元测试,最重要的是有一条规则。

    让它们尽可能简单。

    只需使用最低限度的复杂性,这对于使测试成为可能是绝对必要的。

    越不复杂,越不容易出错。

    最好有一个单独的文件,其中包含所有参数和输出以进行比较和预先读取。

    【讨论】:

    • 如果可以一劳永逸地编写测试,我不必检查它们是否正常工作。这里的重要部分是能够以尽可能少的工作添加新的测试数据。
    【解决方案5】:

    由于您的 Output1 和 Output2 方法非常相似,您可以使用继承方法。然后,如果需要,您可以应用测试参数插入

    public class BaseTest
    {
        private readonly string fileName;
    
        public BaseTest(string fileName)
        {
            this.fileName = fileName;
        }
    
        [ClassInitialize]
        public void Initialize()
        {
            // do your work with fileName
        }
    
        [TestCase]
        public void TestOutput1()
        {
            // test body
        }
    
        [TestCase]
        public void TestOutput2()
        {
            // test body
        }
    }
    
    [TestClass]
    public class TestFile1 : BaseTest
    {
        public TestFile1() : base("file1")
        {
        }
    }
    
    [TestClass]
    public class TestFile2 : BaseTest
    {
        public TestFile2() : base("file2")
        {
        }
    }
    

    【讨论】:

    • 谢谢。然而,这仍然需要为每个文件编写单独的测试类,所以它并没有真正让我达到目标。
    【解决方案6】:

    您实际上可以简化这一点,以便对该库的新测试不一定需要对测试库本身进行代码更改。

    数据驱动单元测试的 MS 文档可以在 here 找到。

    我看到人们对 csv 文件使用类似的东西,然后当需要新的测试时,他们只需在 csv 文件中添加一行。

    另外,我个人喜欢 MSTest 中提供的 DataRow 功能。 可以在here 找到示例 MS Doc。我更喜欢这个选项,尽管一个新的测试用例确实需要一行新的代码。

    它应该会减少整体的代码量。有点像这样。

    [TestClass]
    public class FileClass
    {
        [TestMethod]
        [DataRow("somefile", 3, 345, 333)]
        [DataRow("anotherfile", 4, 291, 330)]
        public void Output1IsValid(string fileName, int parameter, int resultX, int resultY) 
        { 
            var fileParseResult = FileParser.ParseFile(fileName);
            Assert.AreEqual(fileParseResult.Item1, resultX);         
        }
    
    }
    

    【讨论】:

    • 谢谢。不过,有一个重要的要求,我没有具体说明:每个文件只能解析一次,而不是每次测试一次。对此感到抱歉。
    • @Aae 单元测试应该按照定义随着代码库的变化而一遍又一遍地执行,为什么你不能一次解析所有文件。您不需要为此进行测试
    • @pwrigshihanomoronimo 我不确定你的意思。你能详细说明一下吗? ParseFile 方法可能会发生变化。
    • 对不起,我发布此声明时已是深夜,没有理解您的原始问题。我现在看到你想要的是类初始化参数,但我不认为有这样的东西。我猜你的代码是实现这一目标的唯一常用方法
    • @Aae 但是,您仍然可以在测试类中使用继承,例如您的基本测试将具有 Output1 和 Output2 方法以及带有文件名字符串的构造函数。
    猜你喜欢
    • 2016-11-07
    • 1970-01-01
    • 1970-01-01
    • 2012-10-19
    • 2021-12-28
    • 1970-01-01
    • 2011-03-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多