Pasar parámetros complejos a [Teoría]

98

Xunit tiene una característica interesante : puede crear una prueba con un Theoryatributo y poner datos en InlineDataatributos, y xUnit generará muchas pruebas y las probará todas.

Quiero tener algo como esto, pero los parámetros a mi método no son 'simples datos' (como string, int, double), sino una lista de mi clase:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }
zchpit
fuente
3
Si tiene sentido en su entorno, podría hacerlo en F # con mucho menos ruido: - stackoverflow.com/a/35127997/11635
Ruben Bartelink
1
Una guía completa que envía objetos complejos como parámetro a los métodos de prueba de tipos complejos en la prueba unitaria
Iman Bahrampour

Respuestas:

137

Hay muchos xxxxDataatributos en XUnit. Mira, por ejemplo, el PropertyDataatributo.

Puede implementar una propiedad que regrese IEnumerable<object[]>. Cada uno de los object[]que genera este método se "descomprimirá" como parámetros para una única llamada a su [Theory]método.

Otra opción es ClassData, que funciona igual, pero permite compartir fácilmente los 'generadores' entre pruebas en diferentes clases / espacios de nombres, y también separa los 'generadores de datos' de los métodos de prueba reales.

Vea, es decir, estos ejemplos de aquí :

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

Ejemplo de 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(); }
}
quetzalcoatl
fuente
@dcastro: sí, en realidad estoy buscando algunos en documentos originales de xunit
quetzalcoatl
2
@ Nick: Estoy de acuerdo que es similar a PropertyData, sino también, usted ha señalado la razón para ello: static. Eso es exactamente por lo que no lo haría. ClassData es cuando desea escapar de la estática. Al hacerlo, puede reutilizar (es decir, anidar) los generadores más fácilmente.
quetzalcoatl
1
¿Alguna idea de lo que pasó con ClassData? No puedo encontrarlo en xUnit2.0, por ahora, estoy usando MemberData con un método estático, que crea una nueva instancia de clase y devuelve eso.
Erti-Chris Eelmaa
14
@Erti, se usa [MemberData("{static member}", MemberType = typeof(MyClass))]para reemplazar el ClassDataatributo.
Junle Li
6
A partir de C # 6, se recomienda usar la nameofpalabra clave en lugar de codificar el nombre de una propiedad (se rompe fácilmente pero en silencio).
Sara
40

Para actualizar la respuesta de @ Quetzalcoatl: El atributo [PropertyData]ha sido reemplazado por el [MemberData]cual toma como argumento el nombre de cadena de cualquier método estático, campo o propiedad que devuelve un IEnumerable<object[]>. (Me parece particularmente agradable tener un método de iterador que realmente pueda calcular los casos de prueba uno a la vez, produciéndolos a medida que se calculan).

Cada elemento en la secuencia devuelto por el enumerador es un object[]y cada matriz debe tener la misma longitud y esa longitud debe ser el número de argumentos para su caso de prueba (anotado con el atributo [MemberData]y cada elemento debe tener el mismo tipo que el parámetro de método correspondiente . (O tal vez pueden ser tipos convertibles, no lo sé).

(Consulte las notas de la versión de xUnit.net de marzo de 2014 y el parche real con código de ejemplo ).

Davidbak
fuente
2
@davidbak El codplex se ha ido. El enlace no funciona
Kishan Vaishnav
11

Crear matrices de objetos anónimos no es la forma más fácil de construir los datos, así que usé este patrón en mi proyecto

Primero, defina algunas clases compartidas reutilizables

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

}

Ahora su prueba individual y sus datos de miembros son más fáciles de escribir y más limpios ...

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

La Descriptionpropiedad de la cadena es tirarse un hueso cuando uno de sus muchos casos de prueba falla

fíat
fuente
1
Me gusta esto; tiene un potencial real para un objeto muy complejo. Tengo que validar las validaciones en más de 90 propiedades. Puedo pasar un objeto JSON simple, deserializarlo y generar los datos para una iteración de prueba. Buen trabajo.
Gustyn
1
¿No están mezclados los parámetros del método de prueba IsValid? ¿No debería ser IsValid (ingrediente, exprectedResult, testDescription)?
pastacool
9

Supongamos que tenemos una clase de automóvil compleja que tiene una clase de fabricante:

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

Vamos a completar y pasar la clase de Automóvil a una prueba de Teoría.

Así que cree una clase 'CarClassData' que devuelva una instancia de la clase Car como se muestra a continuación:

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

Es hora de crear un método de prueba (CarTest) y definir el coche como parámetro:

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

tipo complejo en teoría

Buena suerte

Iman Bahrampour
fuente
3
Esta respuesta aborda explícitamente la cuestión de pasar un tipo personalizado como entrada de teoría que parece faltar en la respuesta seleccionada.
JD Cain
1
Este es exactamente el caso de uso que estaba buscando, que es cómo pasar un tipo complejo como parámetro a una teoría. ¡Funciona perfectamente! Esto realmente vale la pena probar los patrones de MVP. Ahora puedo configurar muchas instancias diferentes de una Vista en todo tipo de estados y pasarlas todas a la misma Teoría que prueba los efectos que tienen los métodos de Presenter en esa vista. ¡Quiéralo!
Denis M. Kitchen
3

Puedes probar de esta manera:

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

Cree otra clase para contener los datos de prueba:

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;
      }
   }
}
Sandy_Vu
fuente
1

Para mis necesidades, solo quería ejecutar una serie de 'usuarios de prueba' a través de algunas pruebas, pero [ClassData], etc., parecía excesivo para lo que necesitaba (porque la lista de elementos estaba adaptada a cada prueba).

Así que hice lo siguiente, con una matriz dentro de la prueba, indexada desde el exterior:

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

Esto logró mi objetivo, manteniendo clara la intención de la prueba. Solo necesita mantener los índices sincronizados, pero eso es todo.

Se ve bien en los resultados, es plegable y puede volver a ejecutar una instancia específica si obtiene un error:

ingrese la descripción de la imagen aquí

Simon_Weaver
fuente
"Se ve bien en los resultados, es plegable y puede volver a ejecutar una instancia específica si recibe un error". Muy buen punto. Un gran inconveniente deMemberData parece ser que no puede ver ni ejecutar la prueba con una entrada de prueba específica. Apesta.
Oliver Pearmain
De hecho, acabo de descubrir que es posible con MemberData si usa TheoryDatay opcionalmente IXunitSerializable. Más información y ejemplos aquí ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain
1

Así es como resolví tu problema, tuve el mismo escenario. Así que en línea con objetos personalizados y una cantidad diferente de objetos en cada ejecución.

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

Así que esta es mi prueba unitaria, observe el parámetro params . Esto permite enviar un número diferente de objeto. Y ahora mi clase 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();
    }

Espero eso ayude !

Max_Thom
fuente
-1

Supongo que te equivocas aquí. Lo que Theoryrealmente significa el atributo xUnit : desea probar esta función enviando valores especiales / aleatorios como parámetros que recibe esta función bajo prueba. Eso significa que lo que se define como el siguiente atributo, tales como: InlineData, PropertyData, ClassData, etc .. será la fuente de esos parámetros. Eso significa que debe construir el objeto fuente para proporcionar esos parámetros. En su caso, supongo que debería usar el ClassDataobjeto como fuente. Además, tenga en cuenta que ClassDatahereda de: IEnumerable<>- eso significa que cada vez que se utilizará otro conjunto de parámetros generados como parámetros entrantes para la función bajo prueba hasta que IEnumerable<>produzca valores.

Ejemplo aquí: Tom DuPont .NET

El ejemplo puede ser incorrecto: no usé xUnit durante mucho tiempo

Jaspe
fuente