【问题标题】:Pass complex parameters to [Theory]将复杂参数传递给 [Theory]
【发布时间】:2014-04-01 09:03:27
【问题描述】:

Xunit has a nice feature:您可以创建一个带有Theory 属性的测试并将数据放入InlineData 属性中,xUnit 将生成许多测试,并全部测试。

我想要这样的东西,但我的方法的参数不是“简单数据”(如stringintdouble),而是我的班级列表:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }

【问题讨论】:

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


【解决方案1】:

XUnit 中有很多xxxxData 属性。例如,查看 MemberData 属性。

您可以实现一个返回IEnumerable&lt;object[]&gt; 的属性。然后,此方法生成的每个 object[] 都将被“解包”为单个调用 [Theory] 方法的参数。

these examples from here

以下是一些示例,仅供快速浏览。

MemberData 示例:就在这里

public class StringTests2
{
    [Theory, MemberData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }
 
    public static IEnumerable<object[]> SplitCountData => 
        new List<object[]>
        {
            new object[] { "xUnit", 1 },
            new object[] { "is fun", 2 },
            new object[] { "to test with", 3 }
        };
}

XUnit ClassData,它的工作原理相同,但允许在不同类/命名空间的测试之间轻松共享“生成器”,并将“数据生成器”与实际测试方法分开。

类数据示例

public class StringTests3
{
    [Theory, ClassData(typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}
 
public class IndexOfData : IEnumerable<object[]>
{
    private readonly List<object[]> _data = new List<object[]>
    {
        new object[] { "hello world", 'w', 6 },
        new object[] { "goodnight moon", 'w', -1 }
    };
 
    public IEnumerator<object[]> GetEnumerator()
    { return _data.GetEnumerator(); }
 
    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

XUnit >= 2.0:现在有一个 [MemberData] 的“重载”,而不是 ClassData,它允许使用来自其他类的静态成员。下面的示例已更新为使用它,因为 XUnit ClassData,它的工作原理相同,但允许在不同类/命名空间中的测试之间轻松共享“生成器”,并将“数据生成器”与实际测试方法分开。

MemberData 示例:查看其他类型

public class StringTests3
{
    [Theory, MemberData(nameof(IndexOfData.SplitCountData), MemberType = typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}
 
public class IndexOfData : IEnumerable<object[]>
{
    public static IEnumerable<object[]> SplitCountData => 
        new List<object[]>
        {
            new object[] { "hello world", 'w', 6 },
            new object[] { "goodnight moon", 'w', -1 }
        };
}

免责声明 :)

上次在 C# 5.0 和 xunit 2.4.1 上使用 dotnetfiddle.net 检查 @20210903 并失败。我无法将测试运行器混入那个小提琴。但至少它编译得很好。请注意,这最初是几年前写的,事情发生了一些变化。我根据我的预感和 cmets 修复了它们。所以.. 它可能包含不明显的拼写错误,否则会在运行时立即弹出明显的错误,以及牛奶和坚果的痕迹。

【讨论】:

  • @Nick:我同意这与 PropertyData 类似,但您也指出了原因:static。这就是为什么我不会。 ClassData 是您想要摆脱静态的时候。通过这样做,您可以更轻松地重用(即嵌套)生成器。
  • @quetzalcoatl 哦,我明白了。您可能有多个数据源,它们有一些共同点,因此您只需在基类中编写一次,其他人就可以继承它。
  • 您知道 ClassData 发生了什么吗?我在 xUnit2.0 中找不到它,目前,我正在使用 MemberData 和静态方法,它创建类的新实例并返回它。
  • @Erti,使用[MemberData("{static member}", MemberType = typeof(MyClass))] 替换ClassData 属性。
  • 从 C#6 开始,建议使用 nameof 关键字,而不是硬编码属性名称(容易中断但无声)。
【解决方案2】:

更新@Quetzalcoatl 的答案:属性[PropertyData] 已被[MemberData] 取代,该属性将返回IEnumerable&lt;object[]&gt; 的任何静态方法、字段或属性的字符串名称作为参数。 (我发现有一个迭代器方法特别好,它实际上可以一次计算一个测试用例,并在计算时生成它们。)

枚举器返回的序列中的每个元素都是一个object[],并且每个数组的长度必须相同,并且该长度必须是测试用例的参数数量(使用属性[MemberData] 进行注释,并且每个元素必须和对应的方法参数有相同的类型。(或者可能是可转换的类型,我不知道。)

(参见release notes for xUnit.net March 2014the actual patch with example code。)

【讨论】:

  • @davidbak codplex 不见了。链接失效了
  • @KishanVaishnav 没有太多改变 imo,我唯一改变的是属性从 PropertyData 到 MemberData
【解决方案3】:

假设我们有一个复杂的 Car 类,它有一个 Manufacturer 类:

public class Car
{
     public int Id { get; set; }
     public long Price { get; set; }
     public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
    public string Name { get; set; }
    public string Country { get; set; }
}

我们将填写 Car 课程并将其通过理论测试。

因此,创建一个“CarClassData”类,该类返回 Car 类的实例,如下所示:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="country",
                    Name="name"
                  }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

是时候创建一个测试方法(CarTest)并将汽车定义为参数了:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
     var output = car;
     var result = _myRepository.BuyCar(car);
}

**如果您要将汽车对象列表传递给 Theory,请按如下方式更改 CarClassData:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new List<Car>()
                {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="Iran",
                    Name="arya"
                  }
                },
                new Car
                {
                  Id=2,
                  Price=45000,
                  Manufacturer = new Manufacturer
                  {
                    Country="Torbat",
                    Name="kurosh"
                  }
                }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

理论将是:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(List<Car> cars)
{
   var output = cars;
}

祝你好运

【讨论】:

  • 此答案明确解决了将自定义类型作为理论输入传递的问题,该输入似乎从所选答案中丢失。
  • 这正是我一直在寻找的用例,即如何将复杂类型作为参数传递给 Theory。完美运行!这确实为测试 MVP 模式带来了回报。我现在可以在各种状态下设置许多不同的 View 实例,并将它们全部传递到同一个 Theory 中,该 Theory 测试 Presenter 方法对该视图的影响。喜欢它!
  • 如何在汽车类数据中返回多个对象?
  • 添加多个 yield return 语句,具有各种场景,任意数量,您的测试将被执行多次。 andrewlock.net/…
  • @AshA。抱歉看到评论晚了。帖子编辑
【解决方案4】:

创建匿名对象数组并不是构造数据的最简单方法,因此我在项目中使用了这种模式。

首先定义一些可重用的共享类:

//http://stackoverflow.com/questions/22093843
public interface ITheoryDatum
{
    object[] ToParameterArray();
}

public abstract class TheoryDatum : ITheoryDatum
{
    public abstract object[] ToParameterArray();

    public static ITheoryDatum Factory<TSystemUnderTest, TExpectedOutput>(TSystemUnderTest sut, TExpectedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExpectedOutput>();
        datum.SystemUnderTest = sut;
        datum.Description = description;
        datum.ExpectedOutput = expectedOutput;
        return datum;
    }
}

public class TheoryDatum<TSystemUnderTest, TExpectedOutput> : TheoryDatum
{
    public TSystemUnderTest SystemUnderTest { get; set; }

    public string Description { get; set; }

    public TExpectedOutput ExpectedOutput { get; set; }

    public override object[] ToParameterArray()
    {
        var output = new object[3];
        output[0] = SystemUnderTest;
        output[1] = ExpectedOutput;
        output[2] = Description;
        return output;
    }

}

现在您的个人测试和成员数据更易于编写和更清晰...

public class IngredientTests : TestBase
{
    [Theory]
    [MemberData(nameof(IsValidData))]
    public void IsValid(Ingredient ingredient, bool expectedResult, string testDescription)
    {
        Assert.True(ingredient.IsValid == expectedResult, testDescription);
    }

    public static IEnumerable<object[]> IsValidData
    {
        get
        {
            var food = new Food();
            var quantity = new Quantity();
            var data= new List<ITheoryDatum>();
            
            data.Add(TheoryDatum.Factory(new Ingredient { Food = food }                       , false, "Quantity missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity }               , false, "Food missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity, Food = food }  , true,  "Valid" ));

            return data.ConvertAll(d => d.ToParameterArray());
        }
    }
}

字符串Description 属性是当你的许多测试用例之一失败时给自己扔骨头。

【讨论】:

  • 我喜欢这个;对于一个非常复杂的对象,它有一些真正的潜力,我必须验证 90 多个属性的验证。我可以传入一个简单的 JSON 对象,对其进行反序列化,然后为测试迭代生成数据。干得好。
  • IsValid Testmethod 的参数不是混淆了 - 不应该是 IsValid(ingrediant, exprectedResult, testDescription) 吗?
【解决方案5】:

你可以试试这个方法:

public class TestClass {

    bool isSaturday(DateTime dt)
    {
       string day = dt.DayOfWeek.ToString();
       return (day == "Saturday");
    }

    [Theory]
    [MemberData("IsSaturdayIndex", MemberType = typeof(TestCase))]
    public void test(int i)
    {
       // parse test case
       var input = TestCase.IsSaturdayTestCase[i];
       DateTime dt = (DateTime)input[0];
       bool expected = (bool)input[1];

       // test
       bool result = isSaturday(dt);
       result.Should().Be(expected);
    }   
}

创建另一个类来保存测试数据:

public class TestCase
{
   public static readonly List<object[]> IsSaturdayTestCase = new List<object[]>
   {
      new object[]{new DateTime(2016,1,23),true},
      new object[]{new DateTime(2016,1,24),false}
   };

   public static IEnumerable<object[]> IsSaturdayIndex
   {
      get
      {
         List<object[]> tmp = new List<object[]>();
            for (int i = 0; i < IsSaturdayTestCase.Count; i++)
                tmp.Add(new object[] { i });
         return tmp;
      }
   }
}

【讨论】:

    【解决方案6】:

    出于我的需要,我只是想通过一些测试来运行一系列“测试用户”——但是 [ClassData] 等对于我需要的东西来说似乎有点过头了(因为项目列表已本地化到每个测试)。

    所以我做了以下操作,在测试中使用了一个数组 - 从外部索引:

    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    public async Task Account_ExistingUser_CorrectPassword(int userIndex)
    {
        // DIFFERENT INPUT DATA (static fake users on class)
        var user = new[]
        {
            EXISTING_USER_NO_MAPPING,
            EXISTING_USER_MAPPING_TO_DIFFERENT_EXISTING_USER,
            EXISTING_USER_MAPPING_TO_SAME_USER,
            NEW_USER
    
        } [userIndex];
    
        var response = await Analyze(new CreateOrLoginMsgIn
        {
            Username = user.Username,
            Password = user.Password
        });
    
        // expected result (using ExpectedObjects)
        new CreateOrLoginResult
        {
            AccessGrantedTo = user.Username
    
        }.ToExpectedObject().ShouldEqual(response);
    }
    

    这实现了我的目标,同时保持了测试的意图清晰。您只需要保持索引同步即可。

    结果看起来不错,它是可折叠的,如果出现错误,您可以重新运行特定实例:

    【讨论】:

    • “结果看起来不错,它是可折叠的,如果出现错误,您可以重新运行特定实例”。很好的一点。 MemberData 的一个主要缺点似乎是您无法查看或使用特定测试输入运行测试。糟透了。
    • 实际上,我刚刚发现,如果您使用TheoryData 和可选的IXunitSerializable,则可以使用MemberData。更多信息和例子在这里...github.com/xunit/xunit/issues/429#issuecomment-108187109
    【解决方案7】:

    这就是我解决您问题的方法,我遇到了同样的情况。因此,每次运行时与自定义对象和不同数量的对象内联。

        [Theory]
        [ClassData(typeof(DeviceTelemetryTestData))]
        public async Task ProcessDeviceTelemetries_TypicalDeserialization_NoErrorAsync(params DeviceTelemetry[] expected)
        {
            // Arrange
            var timeStamp = DateTimeOffset.UtcNow;
    
            mockInflux.Setup(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>())).ReturnsAsync("Success");
    
            // Act
            var actual = await MessageProcessingTelemetry.ProcessTelemetry(JsonConvert.SerializeObject(expected), mockInflux.Object);
    
            // Assert
            mockInflux.Verify(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>()), Times.Once);
            Assert.Equal("Success", actual);
        }
    

    这是我的单元测试,注意 params 参数。这允许发送不同数量的对象。现在我的 DeviceTelemetryTestData 类:

        public class DeviceTelemetryTestData : IEnumerable<object[]>
        {
            public IEnumerator<object[]> GetEnumerator()
            {
                yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
                yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
                yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
                yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            }
    
            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        }
    

    希望对你有帮助!

    【讨论】:

      【解决方案8】:

      我猜你在这里弄错了。 xUnit Theory 属性的实际含义是:您希望通过发送特殊/随机值作为此被测函数接收的参数来测试此函数。这意味着您定义为下一个属性的内容,例如:InlineDataPropertyDataClassData 等将成为这些参数的来源。这意味着您应该构造源对象以提供这些参数。在您的情况下,我想您应该使用 ClassData 对象作为源。另外 - 请注意 ClassData 继承自:IEnumerable&lt;&gt; - 这意味着每次生成的另一组参数将用作被测函数的传入参数,直到 IEnumerable&lt;&gt; 产生值。

      此处示例:Tom DuPont .NET

      示例可能不正确 - 我很长时间没有使用 xUnit

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-10-27
        • 2017-09-22
        • 1970-01-01
        相关资源
        最近更新 更多