Barista IV

AssetsManager & Unit tests

Maintenant que nous avons introduit l’ensemble des pré-requis nécessaires à la conception de notre moteur, nous allons commencer à entrer dans le vif du sujet. Ce coup-ci nous allons nous focaliser sur la gestion des ressources ainsi que leur utilisation. Nous introduirons également la notion de test unitaires.

La classe AssetsManager

Nous allons démarrer en codant notre propre gestionnaire de ressources, celui-ci va nous permettre de:

  • Télécharger et stocker des ressources.
  • Associer un identifiant unique à chaque ressource.
  • Récupérer une ressource par le biais de son identifiant unique.

On va commencer par le téléchargement de données au format JSON.
Pour cela rien de plus simple, nous pouvons faire appel à fonction fetch.

try{
    const url:string = "http://localhost/data.json";
    const data = await fetch(uri).then(response => response.json());
}
catch(error){
    console.log(error);
}

Dans l’exemple ci-dessus, il te suffit de remplacer « url » par une vraie adresse de fichier json et tu devrais avoir réussi à télécharger et interpréter le fichier en question afin de stocker son contenu, sous forme d’objet exploitable, dans ta constante data. Si le téléchargement ou l’interprétation du résultat au format json a échoué, la clause catch est alors invoquée et l’erreur est envoyée à la console.

Plutôt simple n’est-ce pas ? Si tu as besoin d’un peu plus de détail, je te laisse te rendre sur la page de la documentation de fetch. Maintenant que nous avons notre base de code, essayons d’aller un peu plus loin en encapsulant tout cela dans un objet de type AssetsManager.

Création de la classe AssetsManager

Pour commencer, nous allons créer un fichier nommé AssetsManager.ts que nous placerons directement à la racine de notre dossier « lib ».

Une fois le fichier crée, nous commençons par initialiser nos propriétés. Ici la première des propriétés est naturellement celle qui va s’occuper de stocker nos ressources, c’est pourquoi nous créons une propriété nommée « data » de type Map<string, any> .

export default class AssetsManager {

    private data: Map<string, any>;

    constructor() {
        // on va stocker tout type de données et on va les 
        // associer à un alias, une Map<string, any> est parfaite.
        this.data = new Map<string, any>();
    }
}

Télécharger un fichier .json avec fetch

Maintenant que nous avons un endroit où stocker nos données, il convient d’écrire une méthode nous permettant de télécharger des données, pour cela nous allons une méthode nommée loadJSON au sein de notre classe.

    private loadJSON = (uri: string): Promise<any> => {
        return fetch(uri).then(response => response.json());
    };

Comme on peut le voir, cette méthode nous retourne une Promise<any>, ce qui veut dire qu’une fois la promesse résolue nous devrions en théorie recevoir le contenu de notre fichier JSON sous la forme d’un objet exploitable. Mais notre code ne s’arrête pas là, en effet il nous faut maintenant stocker l’objet au sein de notre propriété data et lui associer un alias.

Tu as surement remarqué que la méthode loadJSON est privée, ce qui signifie qu’elle ne peut être invoquée qu’au sein d’une instance de AssetsManager. Nous allons donc créer une méthode publique nommée load qui prendra en paramètre:

  • L’uri de notre fichier JSON
  • l’alias à associer à l’objet résultant de l’interprétation de ce JSON
  • Le type de données ciblé par le paramètre « uri », en effet, à terme notre AssetsManager ne gérera pas que les JSON, il convient donc de prévoir le coup.
    public load = ( uri: string, 
                    type: string = JSON_TYPE, 
                    alias: string): Promise<any> => {

        let promise: Promise<any> = null;

        switch (type) {
            case JSON_TYPE: promise = this.loadJSON(uri); break;

            default:
                return Promise.reject("unhandled data type");
        }

        return promise.then(
            (data: any) => {
                this.set(data, alias);
                return data;
            }
        );

    };
export const JSON_TYPE: string = "JSON_TYPE";

Comme on peut le voir ici, notre type de fichier est défini au sein d’une constante que nous déclarons dans le même fichier, juste après la classe.
Si le type de donnée envoyé en paramètre ne correspond à aucun type connu, alors nous rejetons d’emblée la promesse.

Stocker la donnée et lui associer un alias

Une fois la promesse résolue, nous stockons la donnée ainsi obtenue au sein de la propriété « data » (notre Map<string,any>) en lui associant la clé envoyée en paramètre, çàd « alias ». Pour cela on emploie une méthode de notre AssetsManager nommée « set ».

    public set = (data: any, alias: string) => {
        this.data.set(alias, data);
    };

Pourquoi ne pas avoir donné manipulé directement la propriété « data » au sein de notre méthode « load » me direz-vous ? Et bien tout simplement pour ne pas être dépendant, plus tard, du type de données utilisé pour « data ». En effet, aujourd’hui j’utilise un Map<string, any>, mais demain je pourrais très bien vouloir changer pour un tableau et dans ce cas, l’appel à la méthode « set » restera transparent pour moi.

De plus, cette méthode « set » me permet également de stocker des données de façon synchrone sur mon AssetsManager, çàd sans avoir obligatoirement recours à « load ».

Le CRUD complet chef !


Bon, maintenant que l’on est capable de stocker des données associées à un alias, on doit être capable de les récupérer à l’aide du même alias. On doit même également être capable de supprimer une donnée à l’aide du même alias. Et enfin, le fin du fin, on doit pouvoir supprimer l’ensemble des données stockées au sein de notre AssetsManager. Voici le code manquant:

    public set = (data: any, alias: string) => {
        this.data.set(alias, data);
    };

    public get = (alias: string): any => {
        return this.data.get(alias);
    };

    public delete = (alias: string): boolean => {
        return this.getAll().delete(alias);
    };

    public destroy = (): void => {
        this.getAll().clear();
    };

Test Driven Development

Le projet Barista se veut différent du projet Tomahawk (dont il est l’honorable descendant) en cela qu’il est prévu qu’il soit entièrement testé et ce, dans le but:

  • D’apporter l’assurance que le code fonctionne comme prévu
  • D’apporter la certitude qu’il n’y a pas de régression dans le code
  • D’avoir un code plus robuste, mieux pensé
  • D’initier la communauté au TDD

C’est quoi TDD ?

TDD est l’acronyme de Test Driven Development çàd en français, Développement piloté par les tests. Dans les faits, cela revient à dire qu’il nous faut en tout premier lieu, et ce avant d’écrire notre code, écrire le code qui va tester un morceau ce morceau de code.

Mais pourquoi faire une telle chose ? Et bien cela répond à une logique très simple, avant d’écrire une fonctionnalité, vous devez savoir ce qu’elle est supposée faire dans telle ou telle situation. Et comment s’assurer que votre code fait le job ? Et bien en écrivant un autre bout de code qui va tester son comportement. Si votre code passe le test, alors il est considéré comme valide.

Mais on perd énormément de temps avec ça non ? On écrit deux fois plus de code.

Et bien pas vraiment, en vérité, cela fait même gagner du temps à la fin.
Car un code testé est un code plus propre, qui a vocation à durer dans le temps. De plus, à chaque fois que l’on ajoute une nouvelle fonctionnalité au code, l’ensemble des tests est de nouveau lancé.

Cela permet de vérifier que la nouvelle fonctionnalité ne fait pas bugger les anciennes, on dit que l’on traque les régressions ! Si le code passe ses propres tests, et que les anciens tests chargés d’évaluer les autres ne passent plus, alors il est fort probable que le nouveau code fasse planter le reste par effet de bord.

Il est donc très fortement conseillé de tester son code. Il est même obligatoire d’écrire le test avant le code à tester, cela permet de ne pas influencer l’écriture du test.

Mais pourquoi tu nous en parles après du coup ?

Ah ça c’est la grande question, et bien tout simplement parce que la première fois, c’est souvent plus simple de montrer un code simple et d’écrire le code de test après. Bon, en vérité, j’ai écris mon code bien proprement avant mon AssetsManager, mais j’ai préféré entamer par ce dernier avant de t’achever avec les tests, pas trop déçu 😉 ?

Et du coup il est maintenant temps de t’expliquer ce que j’ai mis en place au sein du projet barista, car si tu as pull les dernières modifs du dépôt, ce que je te conseille fortement, tu as du en voir passer des modifs !

Différents types de tests

Pour commencer, sache qu’il existe plusieurs types de tests, les plus connus étant les tests unitaires et les tests d’intégration. Les tests d’intégration complètent les tests unitaires. En gros, tester unitairement un moteur de bagnole, c’est s’assurer que chaque pièce fonctionne, tester l’intégration de tes pièces, c’est s’assurer qu’elles collaborent bien. Mais plutôt qu’une longue explication, voici plutôt un schéma !

  • Ici les encadrés verts représentent des tiroirs
  • Les encadrés noirs représentent les structures en bois qui les accueillent
  • L’encadré gris représente le reste du meuble

Dans notre exemple, nous avons deux tiroirs qui, pris séparément, fonctionnent à merveille, mais qui, une fois ensemble, se gênent l’un l’autre.

Le test unitaire, c’est vérifier que chaque tiroir coulisse bien, le test d’intégration, c’est vérifier qu’ils ne se gênent pas l’un l’autre ! Bon maintenant que tu sais à peu près dans quoi tu t’embarques, laisse-moi te présenter Jasmine.

Tests unitaires, Jasmine & Karma

Alors bien sur, on ne parle pas de cette Jasmine là, mais bien de celle-ci: https://jasmine.github.io/ . Jasmine est une bibliothèque qui nous permet de facilement décrire et lancer des tests unitaires. Comme tu peux le voir en surfant sur la documentation, cette bibliothèque peut être utilisée dans de nombreux langages, notamment le javascript (et donc Typescript), ce qui nous arrange. Dans un soucis de transparence, il est important de noter que Barista ne sera verra attribué que des tests unitaires dans un premier temps. Nous allons donc passer beaucoup de temps en compagnie de Jasmine, mais bon, qui oserait s’en plaindre ?

À quoi ça ressemble du code Jasmine ?

Afin de répondre à cette question, nous allons examiner ensemble le code qui nous permet de tester la méthode « get » de notre AssetsManager.

import AssetsManager from "../lib/AssetsManager";

describe(
    "AssetsManager test suite",
    () => {
        it(
            "the ressource should be available at the right alias",
            (done) => {
                const manager: AssetsManager = new AssetsManager();
                const data = { msg: "hello" };
                manager.set(data, "mydata");
                expect(manager.get("mydata")).toBe(data);
                done();
            }
        );
    }
);

Décortiquons un peu ce code, première on peut voir que l’on commence par une fonction nommée « describe« , celle-ci prend 2 paramètres:

  • Une chaîne de caractères permettant de décrire brièvement la suite de tests en cours
  • Une fonction contenant l’ensemble des tests unitaires à éxécuter

Cette fonction nous sert donc à déclarer à Jasmine une nouvelle suite de tests unitaires que l’on regroupe sous un même thème. Ensuite vient la fonction nommée « it« . Cette fonction prend 2 paramètres:

  • Une chaîne de caractères, permettant de décrire brièvement l’objectif du test.
  • Une fonction contenant le code du test lui-même.

Cette fonction sert donc à déclarer un nouveau test à Jasmine, au de la suite de tests en cours. Examinons maintenant le code du test en lui-même:

  • On crée une nouvelle instance d’AssetsManager.
  • Puis on crée un objet avec une propriété « msg » valant « hello ».
  • Puis on stocke cet objet au sein de notre manager à l’aide de sa méthode « set », ce faisant, on lui associe l’alias « myalias ».
  • Enfin on fait appel à la fonction « expect » qui vient avec Jasmine, et qui nous sert à déclarer une « attente » auprès de Jasmine.
  • Cette attente, qui est en fait un objet, doit correspondre à quelque chose, ici on utilise donc le « matcher » toBe.
  • On s’attend donc ici à ce que l’appel à notre méthode « get » à laquelle on a passé l’alias « myalias », renvoie un résultat correspondant en tout point à notre objet stocké précédemment.

Si notre objet récupéré auprès de notre manager correspond à celui passé dans toBe, alors le test est réussi. Et voilà, tu as la base de ce qu’est un test unitaire écrit avec Jasmine. Ici, on teste en réalité deux fonctionnalités, le « setter » et le « getter »…. Bon c’est bien joli tout ça, mais comment je les lance mes tests moi ?

Avoir un bon Karma, c’est important

Pour lancer nos tests avec Jasmine, c’est assez simple, il suffit de le lancer depuis le terminal avec une certaine commande. Cependant cela va nous poser un gros problème, notre code sera testé dans un environnement nodeJS. Et le problème avec l’environnement nodeJS, c’est qu’il ne contient pas nativement une grosse partie des fonctionnalités du navigateur, notamment la fonction fetch, l’accès au DOM, la possibilité de créer une image à la volée etc etc …

Bref, on tente de tester un code à destination du front-end dans un environnement back-end, et ça coince ! Pour ça, nous avons plusieurs solutions, mais la plus simple serait de démarrer nos tests au sein du navigateur et pour ça je me suis tourné vers Karma.

Karma est un outil, que l’on peut installer avec npm (comme Jasmine), et qui va nous aider à lancer nos tests Jasmine au sein d’un ou plusieurs navigateurs. Il possède également de nombreuses fonctionnalités comme la possibilité de vérifier quelle portion de votre code a été testée (code coverage) etc … Grâce à Karma donc, on va pouvoir tester notre code sans trop se prendre la tête. Et il se trouve que je vous ai-même facilité la tâche.

Tester son code avec Barista

Il se trouve que je vous ai simplifié la vie, j’ai moi-même paramétré au sein du dépôt tous les fichiers de configuration pour:

  • Typescript
  • Webpack
  • Jasmine
  • Karma
  • Live-server

En tirant le code de la bonne branche (voir encadré source), vous aurez la bonne configuration, vous aurez juste à lancer la commande npm install afin de télécharger les paquets nécessaires.

Pour tester votre code, il vous suffira alors d’écrire le code dans le dossier test et de respecter l’extension en .spec.ts, puis de lancer la commande suivante dans le terminal « npm test« . Une fenêtre s’ouvre alors dans votre navigateur, puis votre terminal vous indique la proportion de tests réussis ou échoués. Comme vous pouvez le voir dans cet aperçu, l’ensemble des tests de AssetsManager sont passés, d’ailleurs ce code est allé un peu plus loin que dans cet article, avec l’AssetsManager du dépôt vous pouvez:

  • Télécharger un fichier.json et y associer un alias
  • Télécharger un fichier image et y associer un alias
  • Stocker du contenu de façon synchrone et y associer un alias
  • Récupérer du contenu à l’aide d’un alias
  • Détruire un contenu à l’aide d’un alias
  • Détruire l’ensemble des contenus stockés
  • Ajouter un contenu à télécharger à une file d’attente et y associer un alias
  • Télécharger l’ensemble de la file d’attente
  • Vider la file d’attente

Tout ce code est naturellement testé unitairement au sein du fichier « test/AssetsManager.spec.ts« .

Conclusion

Avec cette approche orientée TDD, le code de notre moteur sera beaucoup plus robuste et nous pourrons nous assurer que celui-ci ne régressera pas. Il est désormais possible également de télécharger et de récupérer des ressources qui nous serons bien utiles, notamment au moment où nous commencerons à créer nos classes d’Affichage. C’est tout pour cette fois, on se dit à bientôt, devant un petit café !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *