web-dev-qa-db-fra.com

Les tests MemberData apparaissent comme un test au lieu de plusieurs

Lorsque vous utilisez [Theory] ensemble avec [InlineData] il créera un test pour chaque élément de données en ligne fourni. Cependant, si vous utilisez [MemberData] il apparaîtra comme un seul test.

Existe-t-il un moyen de faire [MemberData] les tests apparaissent comme des tests multiples?

32
NPadrutt

J'ai passé beaucoup de temps à essayer de comprendre celui-ci dans mon projet. Cette discussion Github connexe de @NPadrutt lui-même a beaucoup aidé, mais c'était toujours déroutant.

Le tl; dr est le suivant: [MemberInfo] signalera un test de groupe unique sauf si les objets fournis pour chaque test peuvent être complètement sérialisés et désérialisés en implémentant IXunitSerializable.


Contexte

Ma propre configuration de test était quelque chose comme:

public static IEnumerable<object[]> GetClients()
{
    yield return new object[] { new Impl.Client("clientType1") };
    yield return new object[] { new Impl.Client("clientType2") };
}

[Theory]
[MemberData(nameof(GetClients))]
public void ClientTheory(Impl.Client testClient)
{
    // ... test here
}

Le test a été exécuté deux fois, une fois pour chaque objet de [MemberData], comme prévu. Comme l'a expérimenté @NPadrutt, un seul élément est apparu dans l'Explorateur de tests, au lieu de deux. En effet, l'objet fourni Impl.Client n'était pas sérialisable par l'une ou l'autre des interfaces prises en charge par xUnit (voir plus loin).

Dans mon cas, je ne voulais pas intégrer les problèmes de test dans mon code principal. Je pensais que je pouvais écrire un proxy léger autour de ma vraie classe qui tromperait le coureur xUnit en pensant qu'il pourrait le sérialiser, mais après avoir combattu avec lui plus longtemps que je ne voudrais l'admettre, j'ai réalisé que la partie que je ne comprenais pas était :

Les objets ne sont pas seulement sérialisés pendant la découverte pour compter les permutations; chaque objet est également désérialisé au moment de l'exécution du test au démarrage du test.

Donc, tout objet que vous fournissez avec [MemberData] doit prendre en charge une (dé-) sérialisation aller-retour complète. Cela me semble évident maintenant, mais je n'ai trouvé aucune documentation à ce sujet pendant que j'essayais de le comprendre.


Solution

  • Assurez-vous que chaque objet (et tout élément non primitif qu'il peut contenir) peut être entièrement sérialisé et désérialisé. L'implémentation de IXunitSerializable de xUnit indique à xUnit qu'il s'agit d'un objet sérialisable.

  • Si, comme dans mon cas, vous ne voulez pas ajouter d'attributs au code principal, une solution consiste à créer une classe de générateur sérialisable mince pour les tests qui peut représenter tout ce qui est nécessaire pour recréer la classe réelle. Voici le code ci-dessus, après l'avoir fait fonctionner:

TestClientBuilder

public class TestClientBuilder : IXunitSerializable
{
    private string type;

    // required for deserializer
    public TestClientBuilder()
    {
    }

    public TestClientBuilder(string type)
    {
        this.type = type;
    }

    public Impl.Client Build()
    {
        return new Impl.Client(type);
    }

    public void Deserialize(IXunitSerializationInfo info)
    {
        type = info.GetValue<string>("type");
    }

    public void Serialize(IXunitSerializationInfo info)
    {
        info.AddValue("type", type, typeof(string));
    }

    public override string ToString()
    {
        return $"Type = {type}";
    }
}

Test

public static IEnumerable<object[]> GetClients()
{
    yield return new object[] { new TestClientBuilder("clientType1") };
    yield return new object[] { new TestClientBuilder("clientType2") };
}

[Theory]
[MemberData(nameof(GetClients))]
private void ClientTheory(TestClientBuilder clientBuilder)
{
    var client = clientBuilder.Build();
    // ... test here
}

Il est légèrement ennuyeux de ne plus injecter l'objet cible, mais ce n'est qu'une ligne de code supplémentaire pour invoquer mon générateur. Et, mes tests réussissent (et apparaissent deux fois!), Donc je ne me plains pas.

25
Nate Barbettini

MemberData peut fonctionner avec des propriétés ou des méthodes qui renvoient IEnumerable de l'objet []. Vous verrez un résultat de test distinct pour chaque rendement dans ce scénario:

public class Tests
{ 
    [Theory]
    [MemberData("TestCases", MemberType = typeof(TestDataProvider))]
    public void IsLargerTest(string testName, int a, int b)
    {
        Assert.True(b>a);
    }
}

public class TestDataProvider
{
    public static IEnumerable<object[]> TestCases()
    {
        yield return new object[] {"case1", 1, 2};
        yield return new object[] {"case2", 2, 3};
        yield return new object[] {"case3", 3, 4};
    }
}

Cependant, dès que vous devrez passer des objets personnalisés complexes quel que soit le nombre de cas de test dont vous disposerez, la fenêtre de sortie du test affichera un seul test. Ce n'est pas un comportement idéal et en fait très gênant lors du débogage du scénario de test qui échoue. La solution consiste à créer votre propre wrapper qui dérivera de IXunitSerializable.

public class MemberDataSerializer<T> : IXunitSerializable
    {
        public T Object { get; private set; }

        public MemberDataSerializer()
        {
        }

        public MemberDataSerializer(T objectToSerialize)
        {
            Object = objectToSerialize;
        }

        public void Deserialize(IXunitSerializationInfo info)
        {
            Object = JsonConvert.DeserializeObject<T>(info.GetValue<string>("objValue"));
        }

        public void Serialize(IXunitSerializationInfo info)
        {
            var json = JsonConvert.SerializeObject(Object);
            info.AddValue("objValue", json);
        }
    }

Maintenant, vous pouvez avoir vos objets personnalisés en tant que paramètres de Xunit Theories et les voir/déboguer toujours comme des résultats indépendants dans la fenêtre du testeur:

public class UnitTest1
{
    [Theory]
    [MemberData("TestData", MemberType = typeof(TestDataProvider))]
    public void Test1(string testName, MemberDataSerializer<TestData> testCase)
    {
        Assert.Equal(1, testCase.Object.IntProp);
    }
}

public class TestDataProvider
{
    public static IEnumerable<object[]> TestData()
    {
        yield return new object[] { "test1", new MemberDataSerializer<TestData>(new TestData { IntProp = 1, StringProp = "hello" }) };
        yield return new object[] { "test2", new MemberDataSerializer<TestData>(new TestData { IntProp = 2, StringProp = "Myro" }) };      
    }
}

public class TestData
{
    public int IntProp { get; set; }
    public string StringProp { get; set; }
}

J'espère que cela t'aides.

12
user1807319

Dans mon récent projet, j'ai rencontré le même problème et après quelques recherches, la solution que j'ai trouvée est la suivante:

Implémentez votre MyTheoryAttribute personnalisé étendant FactAttribute avec MyTheoryDiscoverer implémentant IXunitTestCaseDiscoverer et plusieurs MyTestCases personnalisés étendant TestMethodTestCase et implémentant IXunitTestCase à votre convenance. Vos cas de test personnalisés doivent être reconnus par MyTheoryDiscoverer et utilisés pour encapsuler vos cas de test théoriques énumérés sous une forme visible par le cadre Xunit même si les valeurs transmises ne sont pas sérialisées en mode natif par Xunit et n'implémentent pas IXunitSerializable.

Ce qui est le plus important il n'est pas nécessaire de changer votre précieux code sous test !

C'est un peu de travail à faire mais comme il a déjà été fait par moi et est disponible sous la licence MIT, n'hésitez pas à l'utiliser. Il fait partie du projet DjvuNet qui est hébergé sur GitHub.

Le lien direct vers le dossier correspondant avec le code de support Xunit est ci-dessous:

Code de support du test DjvuNet

Pour l'utiliser, créez un assemblage séparé avec ces fichiers ou incluez-les directement dans votre projet de test.

L'utilisation est exactement la même qu'avec Xunit TheoryAttribute et ClassDataAttribute et MemberDataAttribute sont pris en charge c'est-à-dire:

[DjvuTheory]
[ClassData(typeof(DjvuJsonDataSource))]
public void InfoChunk_Theory(DjvuJsonDocument doc, int index)
{
    // Test code goes here
}


[DjvuTheory]
[MemberData(nameof(BG44TestData))]
public void ProgressiveDecodeBackground_Theory(BG44DataJson data, long length)
{
    // Test code goes here
}

Le crédit revient aussi à un autre développeur mais malheureusement je ne trouve pas son repo sur github

3

Pour l'instant, ReSharper peut afficher tous les tests MemberData avec des paramètres personnalisés lorsque vos classes personnalisées remplacent ToString().

Par exemple :

public static TheoryData<Permission, Permission, Permission> GetAddRuleData()
{
    var data = new TheoryData<Permission, Permission, Permission>
    {
        {
            new Permission("book", new[] {"read"}, null),
            new Permission("book", new[] {"delete"}, new[] {"2333"}),
            new Permission("book", new[] {"delete", "read"}, new[] {"*", "2333"})
        },
        {
            new Permission("book", new[] {"read"}, null),
            new Permission("music", new[] {"read"}, new[] {"2333"}), new Permission
            {
                Resources = new Dictionary<string, ResourceRule>
                {
                    ["book"] = new ResourceRule("book", new[] {"read"}, null),
                    ["music"] = new ResourceRule("music", new[] {"read"}, new[] {"2333"}),
                }
            }
        }
    };
    return data;
}

Permission remplace ToString(), puis dans l'explorateur de session de test ReSharper:

xunitR#

1
Tao Zhu