Passe parâmetros complexos para [Teoria]

98

O Xunit tem um recurso interessante : você pode criar um teste com um Theoryatributo e colocar dados nos InlineDataatributos, e o xUnit irá gerar muitos testes e testar todos eles.

Eu quero ter algo como isto, mas os parâmetros para o meu método não são 'dados simples' (como string, int, double), mas uma lista de minha classe:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }
zchpit
fonte
3
Se fizer sentido em seu ambiente, você pode fazer em F # com muito menos ruído: - stackoverflow.com/a/35127997/11635
Ruben Bartelink 01 de
1
Um guia completo que envia objetos complexos como um parâmetro para métodos de teste tipos complexos em teste de unidade
Iman Bahrampour

Respostas:

137

Existem muitos xxxxDataatributos no XUnit. Verifique por exemplo o PropertyDataatributo.

Você pode implementar uma propriedade que retorna IEnumerable<object[]>. Cada um object[]que este método gera será então "desempacotado" como um parâmetro para uma única chamada para o seu [Theory]método.

Outra opção é ClassData, que funciona da mesma forma, mas permite compartilhar facilmente os 'geradores' entre os testes em diferentes classes / namespaces, e também separa os 'geradores de dados' dos métodos de teste reais.

Veja, por exemplo, estes exemplos aqui :

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

Exemplo 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
fonte
@dcastro: sim, na verdade estou procurando alguns documentos originais do xunit
quetzalcoatl
2
@ Nick: Concordo que é semelhante ao PropertyData, mas também, você apontou a razão para isso: static. É exatamente por isso que eu não faria. ClassData é quando você deseja escapar da estática. Ao fazer isso, você pode reutilizar (ou seja, aninhar) os geradores mais facilmente.
quetzalcoatl
1
Alguma ideia do que aconteceu com ClassData? Não consigo encontrar em xUnit2.0, por enquanto, estou usando MemberData com método estático, que cria uma nova instância de classe e retorna isso.
Erti-Chris Eelmaa
14
@Erti, use [MemberData("{static member}", MemberType = typeof(MyClass))]para substituir o ClassDataatributo.
Junle Li
6
A partir do C # 6, era recomendado usar a nameofpalavra - chave em vez de codificar um nome de propriedade (quebra facilmente, mas silenciosamente).
Sara
40

Para atualizar a resposta de @Quetzalcoatl: O atributo [PropertyData]foi substituído pelo [MemberData]que leva como argumento o nome da string de qualquer método estático, campo ou propriedade que retorna um IEnumerable<object[]>. (Acho particularmente bom ter um método iterador que pode realmente calcular os casos de teste um de cada vez, gerando-os à medida que são calculados.)

Cada elemento na sequência devolvida pelo enumerador é um object[]e cada array deve ter o mesmo comprimento e esse comprimento deve ser o número de argumentos para o seu caso de teste (anotado com o atributo [MemberData]e cada elemento deve ter o mesmo tipo que o parâmetro do método correspondente . (Ou talvez possam ser do tipo conversível, não sei.)

(Consulte as notas de lançamento para xUnit.net março de 2014 e o patch real com código de exemplo .)

Davidbak
fonte
2
@davidbak O codplex se foi. O link não está funcionando
Kishan Vaishnav
11

Criar matrizes de objetos anônimos não é a maneira mais fácil de construir os dados, então usei esse padrão em meu projeto

Primeiro defina algumas classes reutilizáveis ​​e compartilhadas

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

}

Agora seu teste individual e dados de membro são mais fáceis de escrever e mais limpos ...

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

A Descriptionpropriedade string serve para atirar em si mesmo quando um de seus muitos casos de teste falhar

decreto
fonte
1
Eu gosto disso; ele tem algum potencial real para um objeto muito complexo. Tenho que validar as validações em mais de 90 propriedades. Posso passar um objeto JSON simples, desserializá-lo e gerar os dados para uma iteração de teste. Bom trabalho.
Gustyn de
1
os parâmetros para o método de teste IsValid não estão misturados - não deveria ser IsValid (ingrediente, exprectedResult, testDescription)?
pastacool
9

Suponha que temos uma classe Car complexa que tem uma classe 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 preencher e passar na classe Carro para um teste de Teoria.

Portanto, crie uma classe 'CarClassData' que retorna uma instância da classe Car como a seguir:

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

É hora de criar um método de teste (CarTest) e definir o carro como parâmetro:

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

tipo complexo em teoria

Boa sorte

Iman Bahrampour
fonte
3
Esta resposta aborda explicitamente a questão de passar um tipo personalizado como a entrada da Teoria que parece faltar na resposta selecionada.
JD Cain
1
Este é exatamente o caso de uso que eu estava procurando, que consiste em passar um tipo complexo como parâmetro para uma teoria. Funciona perfeitamente! Isso realmente vale a pena testar os padrões MVP. Agora posso configurar várias instâncias diferentes de uma visão em todos os tipos de estados e passá-los todos para a mesma teoria que testa os efeitos que os métodos do Presenter têm nessa visão. Adoro!
Denis M. Kitchen,
3

Você pode tentar desta forma:

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

Crie outra classe para conter os dados de teste:

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
fonte
1

Para minhas necessidades, eu só queria executar uma série de 'usuários de teste' por meio de alguns testes - mas [ClassData] etc. parecia um exagero para o que eu precisava (porque a lista de itens foi localizada para cada teste).

Então, fiz o seguinte, com uma matriz dentro do teste - indexada de fora:

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

Isso atingiu meu objetivo, mantendo a intenção do teste clara. Você só precisa manter os índices sincronizados, mas isso é tudo.

Parece bom nos resultados, é recolhível e você pode executar novamente uma instância específica se receber um erro:

insira a descrição da imagem aqui

Simon_Weaver
fonte
"Parece bom nos resultados, é recolhível e você pode executar novamente uma instância específica se obtiver um erro". Muito bom ponto. Uma grande desvantagem de MemberDataparece ser que você não pode ver nem executar o teste com uma entrada de teste específica. É uma merda.
Oliver Pearmain
Na verdade, acabei de descobrir que é possível MemberDatase você usar TheoryDatae opcionalmente IXunitSerializable. Mais informações e exemplos aqui ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain
1

Assim resolvi o seu problema, tive o mesmo cenário. Então, inline com objetos personalizados e um número diferente de objetos em cada execução.

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

Portanto, este é meu teste de unidade, observe o parâmetro params . Isso permite enviar um número diferente de objetos. E agora minha classe 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 que ajude !

Max_Thom
fonte
-1

Eu acho que você se enganou aqui. O que o Theoryatributo xUnit realmente significa: você deseja testar esta função enviando valores especiais / aleatórios como parâmetros que esta função em teste recebe. Isso significa que o que você define como o próximo atributo, como: InlineData, PropertyData, ClassData, etc .. será a fonte para esses parâmetros. Isso significa que você deve construir o objeto de origem para fornecer esses parâmetros. No seu caso, acho que você deve usar o ClassDataobjeto como fonte. Além disso - observe que ClassDataherda de: IEnumerable<>- isso significa que cada vez que outro conjunto de parâmetros gerados será usado como parâmetros de entrada para a função em teste até IEnumerable<>produzir valores.

Exemplo aqui: Tom DuPont .NET

O exemplo pode estar incorreto - eu não usei o xUnit por muito tempo

Jaspe
fonte