将复杂参数传递给[理论]

时间:2014-02-28 11:25:08

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

Xunit has a nice feature:您可以使用Theory属性创建一个测试,并将数据放入InlineData属性中,xUnit将生成许多测试,并对所有测试进行测试。

我希望有类似的内容,但我方法的参数不是“简单数据”(例如stringintdouble),而是我的班级列表:

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

8 个答案:

答案 0 :(得分:118)

XUnit中有许多xxxxData个属性。查看例如PropertyData属性。

您可以实现返回IEnumerable<object[]>的属性。然后,此方法生成的每个object[]将“解压缩”为单个调用[Theory]方法的参数。

另一个选项是ClassData,它的工作方式相同,但允许在不同类/命名空间的测试之间轻松共享“生成器”,并且还将“数据生成器”与实际测试方法分开。

见ie these examples from here

PropertyData示例

public class StringTests2
{
    [Theory, PropertyData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }

    public static IEnumerable<object[]> SplitCountData
    {
        get
        {
            // Or this could read from a file. :)
            return new[]
            {
                new object[] { "xUnit", 1 },
                new object[] { "is fun", 2 },
                new object[] { "to test with", 3 }
            };
        }
    }
}

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(); }
}

答案 1 :(得分:33)

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

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

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

答案 2 :(得分:8)

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

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

//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, TExecptedOutput> : 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, string testDescription, bool expectedResult)
    {
        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属性是在您的许多测试用例中的一个失败时将自己投入骨骼

答案 3 :(得分:2)

您可以尝试这种方式:

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;
      }
   }
}

答案 4 :(得分:2)

假设我们有一个复杂的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);
}

complex type in theory

祝你好运

答案 5 :(得分:1)

在相同的情况下,这就是我解决您的问题的方式。因此,可以内联自定义对象,并且每次运行时可以容纳不同数量的对象。

    [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();
    }

希望有帮助!

答案 6 :(得分:0)

出于我的需要,我只想通过一些测试来运行一系列的“测试用户”-但是[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);
}

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

看起来不错,可以折叠,如果遇到错误,可以重新运行特定实例:

enter image description here

答案 7 :(得分:-1)

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

此处示例:Tom DuPont .NET

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