Pour que les clients mails (MUA) puissent accéder aux serveurs, ils doivent être configurés correctement. Il existe plusieurs méthodes d’auto-configuration:
J’ai récemment travaillé sur un site WordPress, pour un client qui n’avait pas la main sur le serveur hébergeant son site. Comme à mon habitude, je travaillais sur une copie locale du site, jusqu’au moment de la mise en prod. Et là, surprise: la config PHP ne permet pas d’uploader des fichiers de plus de 2 Mo. Je ne peux donc pas installer mes extensions WordPress via l’interface classique de WordPress.
Pour palier à ce problème, voici un petit hack rapide, qui permet de contourner cette limitation: j’utilise une petite fonction PHP pour récupérer et installer les extensions que je veux, depuis une URL.
La fonction exécute l’installation seulement si le plugin n’est pas déjà installé, en utilisant le code standard de WordPress. L’exécution se fait à la prochaine visite d’une page admin, et affiche un retour visuel comme une installation via la page dédiée. Il suffit d’ajouter le code dans le thème (dans Apparence → Éditeur de thème → fichier functions.php), se rendre sur n’importe quelle page, puis retirer le code du thème. Et voilà ! Il ne reste plus qu’à l’activer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
functionensures_plugin_is_installed($plugin_name, $plugin_url) { include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; add_action('admin_notices', function() use ($plugin_name, $plugin_url) { foreach (get_plugins() as$key => $value) { if (strpos($key, $plugin_name . '/') === 0) return; // the plugin is already installed } print('Will install plugin: ' . $plugin_url);
$upgrader = new Plugin_Upgrader(); $upgrader->install($plugin_url); }); }
// How to use it: $ebak_plugin_name = 'plugin-installed-from-public-url'; $ebak_plugin_url = "https://www.example.com/plugin/plugin-installed-from-public-url.zip"; ensures_plugin_is_installed($ebak_plugin_name, $ebak_plugin_url);
Bien sûr, c’est un hack très grossier, on peut améliorer le code. Mais ça fait ce que je veux, mon problème est résolu.
Sur le développement de Serenity, l’un de mes projets actuels, je suis tombé sur une situation où je veux créer une tâche de fond périodique. J’essaye d’appliquer la méthode TDD. Je commence donc par créer un fichier de test. Et là, je m’aperçois que je n’ai pas souvenir d’avoir déjà fait des tests propres impliquant la gestion du temps avec NodeJs.
Ma situation
J’utilise principalement MochaJs pour mes tests, et je me contente du module inclus dans NodeJs pour les assertions. En plus de cela, j’ai quelques libs pour tester des points spécifiques (nock pour le réseau), mais je n’ai jamais utilisé de bibliothèque de Mock/Spies/Stubs.
En interne, j’utilise setTimeout(). Je veux pouvoir tester qu’une tâche est bien créée, tester l’exécution de cette tâche, mais aussi être sûr qu’il ne reste aucun timer à la fin d’un test. Pour les petites durées, une solution est d’attendre que le temps passe. C’est envisageable pour des tâches de quelques millisecondes, mais ce n’est pas le cas ici. La deuxième solution évidente est de coder à la main un mock des fonctions de timer. Mais cela compliquerait le code des tests unitaires: il y a plusieurs fonctions à remplacer et c’est propice aux erreurs. On veut garder le code des tests unitaires le plus simple possible.
Un tour des solutions existantes
Jasmine est un autre framework de test, un concurrent de Mocha avec plus de fonctionnalités. Il propose une classe Clock, qui permet de manipuler le temps: on peut définir la date actuelle, et faire “avancer” le temps. Dans nos tests, on va alors tester le résultat (Est-ce que la tâche a été exécutée à t=X ?) sans se préoccuper de l’implémentation interne. En fait, on n’a pas moyen de voir ce qu’il se passe en interne, l’API de Jasmine est trop limitée. Par exemple, il est impossible de savoir si un timer a été créé en arrière-plan sans faire avancer le temps pour exécuter ce timer.
Jest est un troisième framework de test, lui aussi batteries incluses. Sa gestion des mock timers permet plus de chose que Jasmine: on peut avancer dans le temps, mais aussi tester la présence d’appel aux méthodes natives des timers, et avancer jusqu’à exécution des timers existants.
Ces deux outils ne sont pas compatibles avec Mocha, mais c’est intéressant de voir ce qu’il se fait ailleurs.
Contrairement aux autres frameworks de tests, MochaJs ne possède que très peu d’outils pour faciliter les tests. L’utilisation habituelle est d’ajouter une bibliothèque dédiée aux Mock/Stubs/Spies. La plus populaire d’entre-elles est SinonJS. C’est celle que l’on utilisera.
SinonJS propose des objets Fake timers. La documentation, de prime abord pas très détaillée, cache en réalité une bibliothèque très complète de gestion du temps: Lolex. Il inclut toutes les fonctions que l’on peut souhaiter: en plus du mock des fonctions de timer, on retrouve le support de process.nextTick(), de requestAnimationFrame(), ainsi que des points particuliers comme la simulation du changement de l’heure du système.
Cas particulier de test: Tâche périodique et Promise
Ma tâche de fond exécute une fonction asynchrone de façon périodique. Cette fonction elle-même peut prendre du temps, et retourne donc une Promise. J’utilise setTimeout pour relancer la tâche à la fin de chaque exécution plutôt que setInterval, qui pourrait poser des problèmes si la tâche prend trop de temps à s’exécuter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Version très simplifiée de ma tâche périodique classMyPeriodicTask() { constructor() { setTimeout(3600 * 1000, this._executeTask()); }
// On créé un nouveau contexte pour chaque test, qui va capturer les appels à `setTimeout`. // Les timers existants à la fin d'un test sont supprimés. let clock = null; beforeEach(() => clock = sinon.useFakeTimers()); afterEach(() => clock.restore());
it('should set a new timer after the task has been executed', function() { // Le 1er timer est créé new MyPeriodicTask(); // On vérifie avec clock: il y a bien 1 timer enregistré. assert.deepStrictEqual(clock.countTimers(), 1); // On déclenche ce timer. Cela va appeler la task. clock.runToLast();
// On veut que le prochain timer soit présent. assert.deepStrictEqual(clock.countTimers(), 1); }); });
Et là, … c’est le drame!
1 2 3 4 5
> mocha 1) MyPeriodicTask should set a new timer after the task has been executed: AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: 0 != 1
Le problème ? La tâche de fond est asynchrone. Le nouveau timer n’est créé que lorsque la Promise est résolue.
Pour tester correctement, je dois attendre la résolution de la Promise. Pour cela, je vais utiliser setImmediate(). En effet, cette fonction permet de placer une callback qui sera toujours appelée après la résolution de toutes les chaînes de Promises qui n’utilisent pas des fonctions bloquantes. Oui, le nom de la fonction est trompeur.
// Évidemment, 'lolex' va créer un fake 'setImmediate'. // On garde une référence vers la véritable fonction. let realSetImmediate = setImmediate;
functionwaitForPromiseResolution() { // Cette fonction retourne elle-même une Promise, mais ... // celle-ci ne sera résolue que lorsque toutes les Promises non bloquantes auront été exécutées. returnnewPromise(r => realSetImmediate(r)); }
// NOTE: le test est maintenant une fonction async. it('should set a new timer after the task has been executed', asyncfunction() { // Le 1er timer est créé new MyPeriodicTask(); // On vérifie avec clock: il y a bien 1 timer enregistré. assert.deepStrictEqual(clock.countTimers(), 1); // On déclenche ce timer. Cela va appeler la task. clock.runToLast(); // On attend que toute les promises soient résolues. await waitForPromiseResolution();
// Hourra! Le prochain timer est enregistré. assert.deepStrictEqual(clock.countTimers(), 1); });
L’astuce du setImmediate() ne marche que pour certains cas particuliers: quand la tâche de fond ne fait pas d’IO. On arrive là sur un problème de conception: il faut que le code sous-jacent devrait toujours être découpé et pensé pour être testable. Dans mon cas d’utilisation réelle, la fonction _task() est ajoutée par injection de dépendance. La gestion des timers et la tâche elle-même sont deux fonctionnalités distinctes, elles sont donc découplées. Dans les tests, je passe la tâche que je veux.
J’ai toujours pris des notes sur des sujets diverses et variés, que ce soit pour collecter des informations, mettre en forme mes conaissances, ou évacuer mes pensées. J’ai pensé plusieurs fois à ouvrir un blog, mais je n’ai jamais trouvé la motivation et l’énergie de retravailler mes brouillons pour en faire quelque chose de présentable. Ceci est donc une tentative de publier mes écrits.
Je ne sais pas encore exactement quels sujets seront traités, mais on peut s’attendre à des posts techniques sur le dévelopement, l’admin. sys., ou plus généralement l’informatique. Plus particulierement:
Je travaille beaucoup avec Javascript et NodeJs
Je m’intérèsse à l’univers des crypto-monnaies
J’apprécie et je soutiens le monde de l’open-source et du libre
Le développement de jeu vidéo est un sujet qui m’attire