Passa parametri complessi a [Teoria]


98

Xunit ha una caratteristica interessante : puoi creare un test con un Theoryattributo e inserire i dati negli InlineDataattributi, e xUnit genererà molti test e testarli tutti.

Voglio avere qualcosa di simile, ma i parametri al mio metodo non sono 'dati semplici' (come string, int, double), ma un elenco della mia classe:

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

3
Se ha un senso nel proprio ambiente, è possibile farlo in F # con molto meno rumore: - stackoverflow.com/a/35127997/11635
Ruben Bartelink

1
Una guida completa che invia oggetti complessi come parametro ai metodi di test tipi complessi in Unit test
Iman Bahrampour

Risposte:


137

Ci sono molti xxxxDataattributi in XUnit. Controlla ad esempio l' PropertyDataattributo.

Puoi implementare una proprietà che restituisce IEnumerable<object[]>. Ognuno object[]generato da questo metodo verrà quindi "decompresso" come parametri per una singola chiamata al [Theory]metodo.

Un'altra opzione è ClassData, che funziona allo stesso modo, ma consente di condividere facilmente i "generatori" tra i test in diverse classi / spazi dei nomi, e separa anche i "generatori di dati" dai metodi di test effettivi.

Vedi ad esempio questi esempi da qui :

Esempio di 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 Esempio

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

@dcastro: sì, sono in realtà alla ricerca di un po 'su documenti originali xUnit
Quetzalcoatl

2
@ Nick: Sono d'accordo che è simile a PropertyData, ma anche, vi ho fatto notare la ragione per questo: static. Questo è esattamente il motivo per cui non lo farei. ClassData è quando vuoi uscire dalla statica. In questo modo, puoi riutilizzare (cioè annidare) i generatori più facilmente.
quetzalcoatl

1
Qualche idea su cosa è successo con ClassData? Non riesco a trovarlo in xUnit2.0, per ora sto usando MemberData con il metodo statico, che crea una nuova istanza di classe e la restituisce.
Erti-Chris Eelmaa

14
@ Erti, usa [MemberData("{static member}", MemberType = typeof(MyClass))]per sostituire l' ClassDataattributo.
Junle Li

6
A partire da C # 6 si consiglia di utilizzare la nameofparola chiave invece di codificare un nome di proprietà (si rompe facilmente ma silenziosamente).
sara

40

Per aggiornare la risposta di @ Quetzalcoatl: l'attributo [PropertyData]è stato sostituito dal [MemberData]quale prende come argomento il nome della stringa di qualsiasi metodo, campo o proprietà statica che restituisce un file IEnumerable<object[]>. (Trovo particolarmente bello avere un metodo iteratore che possa effettivamente calcolare i casi di test uno alla volta, rendendoli così come vengono calcolati.)

Ogni elemento nella sequenza restituita dall'enumeratoreè un object[]e ogni array deve avere la stessa lunghezza e quella lunghezza deve essere il numero di argomenti del tuo caso di test (annotato con l'attributo [MemberData]e ogni elemento deve avere lo stesso tipo del parametro del metodo corrispondente (O forse possono essere tipi convertibili, non lo so.)

(Vedere le note di rilascio per xUnit.net marzo 2014 e la patch effettiva con il codice di esempio .)


2
@davidbak Il codplex è andato. Il collegamento non funziona
Kishan Vaishnav

11

La creazione di array di oggetti anonimi non è il modo più semplice per costruire i dati, quindi ho usato questo modello nel mio progetto

Definisci prima alcune classi condivise e riutilizzabili

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

}

Ora il tuo test individuale e i dati dei membri sono più facili da scrivere e più puliti ...

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 Descriptionproprietà della stringa è di gettarti un osso quando uno dei tuoi tanti casi di test fallisce


1
Mi piace questo; ha un potenziale reale per un oggetto molto complesso devo convalidare le convalide su 90+ proprietà. Posso passare un semplice oggetto JSON, deserializzarlo e generare i dati per un'iterazione di prova. Buon lavoro.
Gustyn

1
i parametri per il metodo IsValid Test non sono confusi - non dovrebbe essere IsValid (ingrediant, exprectedResult, testDescription)?
pastacool

9

Supponiamo di avere una classe Car complessa che ha una classe 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; }
}

Riempiremo e passeremo la classe Auto a un test di teoria.

Quindi crea una classe 'CarClassData' che restituisca un'istanza della classe Car come di seguito:

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

È il momento di creare un metodo di prova (CarTest) e definire l'auto come parametro:

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

tipo complesso in teoria

In bocca al lupo


3
Questa risposta affronta esplicitamente la questione del passaggio di un tipo personalizzato come input teorico che sembra mancare dalla risposta selezionata.
JD Cain

1
Questo è esattamente il caso d'uso che stavo cercando, ovvero come passare un tipo complesso come parametro a una teoria. Funziona perfettamente! Questo paga davvero per testare i modelli MVP. Ora posso impostare molte istanze diverse di una vista in tutti i tipi di stati e passarle tutte nella stessa teoria che verifica gli effetti che i metodi di Presenter hanno su quella vista. Lo adoro!
Denis M. Kitchen

3

Puoi provare in questo modo:

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

Crea un'altra classe per contenere i dati del test:

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

1

Per le mie esigenze volevo solo eseguire una serie di "utenti di prova" attraverso alcuni test, ma [ClassData] ecc. Sembrava eccessivo per ciò di cui avevo bisogno (perché l'elenco degli elementi era localizzato per ogni test).

Quindi ho fatto quanto segue, con un array all'interno del test, indicizzato dall'esterno:

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

Questo ha raggiunto il mio obiettivo, pur mantenendo chiaro l'intento del test. Hai solo bisogno di mantenere sincronizzati gli indici, ma questo è tutto.

Ha un bell'aspetto nei risultati, è comprimibile e puoi rieseguire un'istanza specifica se ricevi un errore:

inserisci qui la descrizione dell'immagine


"Ha un bell'aspetto nei risultati, è comprimibile e puoi rieseguire un'istanza specifica se ottieni un errore". Ottimo punto. Uno dei principali svantaggi di MemberDatasembra essere che non è possibile visualizzare né eseguire il test con un input di test specifico. Fa schifo.
Oliver Pearmain,

In realtà, ho appena capito che è possibile con MemberDatase usi TheoryDatae opzionalmente IXunitSerializable. Maggiori informazioni e exmaples qui ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain

1

Ecco come ho risolto il tuo problema, avevo lo stesso scenario. Quindi in linea con gli oggetti personalizzati e un numero diverso di oggetti su ogni esecuzione.

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

Quindi questo è il mio test unitario, nota il parametro params . Questo permette di inviare un diverso numero di oggetti. E ora la mia 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();
    }

Spero che sia d'aiuto !


-1

Immagino che ti sia sbagliato qui. Cosa Theorysignifica effettivamente l'attributo xUnit : vuoi testare questa funzione inviando valori speciali / casuali come parametri ricevuti da questa funzione sottoposta a test. Ciò significa che quello che si definisce come l'attributo successivo, come ad esempio: InlineData, PropertyData, ClassData, ecc .. sarà la fonte di tali parametri. Ciò significa che dovresti costruire l'oggetto sorgente per fornire quei parametri. Nel tuo caso immagino che dovresti usare ClassDataobject come sorgente. Inoltre, si noti che ClassDataeredita da: IEnumerable<>ciò significa che ogni volta che un altro set di parametri generati verrà utilizzato come parametri in ingresso per la funzione in fase di test finché non IEnumerable<>produce valori.

Esempio qui: Tom DuPont .NET

L'esempio potrebbe non essere corretto: non ho usato xUnit per molto tempo

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.