Test Unitaires – Improuver vos tests avec des attributs custom (JSON, XML)

Test Unitaires – Improuver vos tests avec des attributs custom (JSON, XML)

2018-08-30 0 By Nordes

Simplifiez vos tests lorsque vous consommez des API’s externes (JSon/Xml ou autre).

Quel Framework utiliser?

Sous .Net en général, il y a deux framework de tests plutôt populaire. Le premier étant celui fourni par Microsoft et qui se nomme MS Tests. Le second est XUnit et ce sera sur celui-ci que je vais traiter aujourd’hui.

Je ne vous ferai pas un cours sur comment utiliser XUnit, mais plutôt, comment enrichir cet environnement avec des attributs.

Création simplement d’Attributs ou aussi de Discoverer?

Ici ça comment a ce complexifier. La documentation sur ces deux points n’est pas très claire et la seule façon de comprendre mieux est d’ouvrir directement le code de XUnit afin de comprendre le fonctionnement. Malgré que je vous incite, je ne m’élaborerai pas plus sur ce sujet.

En fait les Attributs et les Discoverers fonctionne plus ou moins de la même façon. Le discoverer appel l’attribut et voilà. Par défaut, si je ne me trompe pas, les attributs seront automatiquement attribué à un discoverer par défaut. À priori, le discoverer va appeler la methode GetData de l’attribut.

Attention! À la compilation, comme à l’exécution de vos tests, votre code sera exécuter. Donc, si vous créez des attributs, assurez-vous que l’exécution est rapide. Si par exemple vous avez a appeler un service API à partir de l’attribut, c’est un très mauvais choix du fait que l’API pourrait être KO ou bien le fait que ce soit lent. Un test unitaire, on s’entend, ça doit s’exécuter le plus rapidement possible.

Quels sont les restrictions d’un attribut?

À priori, on pourrait s’attendre de pouvoir utiliser les objets que l’on désir. Cependant, ce n’est pas le cas. Et sur ce, je vous met un bémole. En fait oui, vous pouvez utiliser les objets que vous désirez. Mais dans ce cas, la phase de reconnaissance de vos tests ne se passeras pas nécessairement comme vous le voudriez. Par exemple, si vous avez 2 Attributs sous votre Theory, vos tests ne vont pas tous s’afficher dans votre explorateur de tests.

  1. 1 test d’affiché sur l’explorateur de tests.
  2. 2 attributs custom afin de pouvoir exécuter 2 tests.
  3. Les résultats des tests (2)
  4. Non indiqué, mais vous verrai si vous créer vos attributs. Le nombre total de tests à exécuter dans l’explorateur est erroné. Les tests ne sont pas comptabilisé dû à une mauvaise reconnaissance de vos attributs (retourne un type différent de ce que XUnit Framework s’attend).
    1. Note bene: Si vous utilisez R#, il est fort possible que vous aillez les tests d’affichés 😉 +1.

Au cas où vous exécuter le tout en console (dotnet tests ton.csproj), vous n’aurez pas de soucis. Tous les tests sont bien exécuté et le compte sera bon. Du coup pour votre CI/CD, tout ira bien. C’est purement estétique et visuel pour les développeurs d’avoir les bonnes informations dans les tests de façon efficace.

Type de retour accepté pour une reconnaissance de vos attributs à tout coup!

  • ==> IXunitSerializable <=== Nous allons traiter de celui-ci (aussi très peu documenté)
  • char, string
  • byte, sbyte
  • short, ushort, int, uint, long, ulong, float, double, decimal
  • bool
  • DateTime
  • DateTimeOffset
  • Type (Le type du Type)
  • Enum
  • Array (mais dont l’Array contiendra des sous-types précédents)

Si vous désirez être à jour dans les types, allez visiter le code officiel 😉

Préface à la construction d’un attribut JsonFileData

Pour cette partie, je vais me baser sur mon code source HoNoSoFt.XUnit.Extensions. C’est en travaillant sur ce projet dont je croyais que j’allais prendre quelques minutes et qu’au final ça m’a pris quelques soirées à réaliser. La plus grande difficulté que j’ai rencontré, était le fait qu’il y a peu ou pas de documentation. La seule façon de comprendre le fonctionnement est soit de rechercher dans les commentaires sous GitHub ou bien d’ouvrir le code source. Évidemment, j’ai choisi la deuxième option, tout simplement parce que je voulais enrichir mes connaissances sur le fonctionnement de XUnit. Si vous êtes un jeune développeur, c’est une bonne façon d’apprendre. La lecture du code des autres, surtout sur des produits assez reconnu, nous apprends beaucoup de choses, même lorsque l’on est un très bon développeur.

S’il-vous-plait, si vous avez à utiliser le code qui suit, veuillez simplement utiliser la référence Nuget. Le package est plutôt minimalistique, même pour ce qui est des dépendances.

Construction de l’attribut JsonFileData

Commençons par du code (simplifier pour un attribut simple):

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Newtonsoft.Json;
using Xunit.Sdk;

namespace HoNoSoFt.XUnit.Extensions
{
    /// <summary>
    /// Usage of the JsonFileData attribute gives the opportunity to load json data
    /// in your tests.
    /// </summary>
    public class JsonFileDataAttribute : DataAttribute
    {
        private readonly string _filePath;
        private readonly Type _type = null;
        private readonly object[] _data;

        /// <inheritdoc />
        public JsonFileDataAttribute(string filePath, params object[] data)
        {
            _filePath = filePath; // Could also look if this is inline json.
            _data = data;
        }

        /// <inheritdoc />
        public JsonFileDataAttribute(string filePath, Type type, params object[] data)
            : this(filePath, data)
        {
            _type = type;
        }

        /// <inheritdoc />
        public override IEnumerable<object[]> GetData(MethodInfo testMethod)
        {
            if (testMethod == null) { throw new ArgumentNullException(nameof(testMethod)); }

            // Get the absolute path to the JSON file
            var path = Path.IsPathRooted(_filePath)
                ? _filePath
                : Directory.GetCurrentDirectory() + "/" + _filePath;
            // Original code (Core 2.1, can't work in Standard2.0, maybe when 2.1 arrives) :(
            //: Path.GetRelativePath(Directory.GetCurrentDirectory(), _filePath);

            var type = testMethod.GetParameters()[0].ParameterType;

            var fileData = LoadFile(path);
            var result = new List<object>(_data);
            result.Insert(0, JsonConvert.DeserializeObject(fileData, type));

            return new[] { result.ToArray() };
        }

        private static string LoadFile(string path)
        {
            if (!File.Exists(path))
            {
                // Maybe we should return null, and then create an empty argument when not found.
                throw new ArgumentException($"Could not find file at path: {path}");
            }

            var fileData = File.ReadAllText(path);
            return fileData;
        }
    }
}

Création d’un test

Comme dit précédemment, ici vous avez la version qui ne vous indiquera pas tous vos tests si vous avez des tests comme ce qui suit:

/// <summary>
/// Tests ok, however, not displayed in normal Test Explorer as separated tests
/// </summary>
/// <param name="mySample">My sample.</param>
/// <param name="expectedResult">The expected result.</param>
[Theory]
[JsonFileData("./assets/sample.json", "data")]
[JsonFileData("./assets/sample2.json", "data2")]
public void JsonFileData_WhenMultipleNotTyped_ExpectOneGlobalInTestExplorer(SampleFakeObject mySample, string expectedResult)
{
    Assert.Equal(expectedResult, mySample.SampleProp);
}

Évolution avec l’utilisation de IXunitSerializable

Mais qu’est-ce que IXunitSerializable? C’est simplement une interface fait maison par l’équipe XUnit afin de réaliser une sérialization rapide. À priori, j’aurais préféré qu’ils donne l’opportunité de retourner un objet déjà typé. Mais si nous utilisons les objets typés, du coups on perds nos tests de notre liste de test.

Lors de l’implémentation, on doit sauvegarder les objets sous forme serializable avec un type de base (string par exemple). Du coup, votre implementation pour du JSON ressemblera très probablement à ce qui suit:

using Newtonsoft.Json;
using System;
using Xunit.Abstractions;

namespace HoNoSoFt.XUnit.Extensions
{
    public class JsonData<T> : JsonData
    {
        public new T Data { get; private set; }

        public JsonData(string originalJson) : base(originalJson, typeof(T))
        {
        }
    }

    public class JsonData : IXunitSerializable
    {
        private readonly Type _type;

        public object Data { get; private set; }
        public string Original { get; private set; }

        public JsonData(string originalJson, Type type)
        {
            Data = JsonConvert.DeserializeObject(originalJson, type);
            Original = originalJson;
            _type = type;
        }

        public void Deserialize(IXunitSerializationInfo info)
        {
            Original = info.GetValue<string>("rawJson");
            Data = JsonConvert.DeserializeObject(Original, _type);
        }

        public void Serialize(IXunitSerializationInfo info)
        {
            info.AddValue("rawJson", Original);
        }
    }
}

Vous voyez, c’est simple 😉 et j’ai même utiliser Newtonsoft.Json pour ce faire.

ensuite si vous modifiez un peu le code précédent avec:

// ...
var fileData = LoadFile(path);
var result = new List<object>(_data);
if (_type != null)
{
    result.Insert(0, new JsonData(fileData, _type));
}
else
{
    if (type == null)
    {
        // This will return a JObject.
        result.Insert(0, JsonConvert.DeserializeObject<object>(fileData));
    }
    else
    {
        result.Insert(0, JsonConvert.DeserializeObject(fileData, type));
    }
}

return new[] { result.ToArray() };
var fileData = LoadFile(path);
var result = new List<object>(_data);
if (_type != null)
{
    result.Insert(0, new JsonData(fileData, _type));
}
else
{
    if (type == null)
    {
        // This will return a JObject.
        result.Insert(0, JsonConvert.DeserializeObject<object>(fileData));
    }
    else
    {
        result.Insert(0, JsonConvert.DeserializeObject(fileData, type));
    }
}

return new[] { result.ToArray() };
// ...

Ensuite, vous n’aurez qu’à mettre à jour vos tests

/// <summary>
/// Jsons the file attribute when attribute multiple expect visible in test explorer.
/// </summary>
/// <param name="mySample">My sample.</param>
/// <param name="expectedResult">The expected result.</param>
[Theory]
[JsonFileData("./assets/sample.json", typeof(SampleFakeObject), "data")] // Le type doit être transmis, sinon vous ne pourrez pas avoir votre objet JsonData correctement
[JsonFileData("./assets/sample2.json", typeof(SampleFakeObject), "data2")]
public void JsonFileData_WhenMultipleNotTyped_ExpectAllVisibleInTestExplorer(JsonData mySample, string expectedResult)
{
    Assert.Equal(typeof(SampleFakeObject), mySample.Data.GetType());
    Assert.Equal(expectedResult, (mySample.Data as SampleFakeObject)?.SampleProp);
}

Résultat

Pourquoi utiliserais-je cet attribut?

Comme dit plus tôt, pour tester votre code sans appeler des API et sans non plus polluer vos tests.

Conclusion

Vous venez de créer votre premier attribut XUnit. Sans doute que vous n’en créerez pas des tonnes, mais c’est bon de savoir comment ça fonctionne. Donc je vous incite encore fortement à lire le code de XUnit sur GitHub ainsi que mes attributs custom sous la librairie HoNoSoFt.XUnit.Extensions.