Création d’un test INGInious¶
Prenons un premier exemple simple qui a pour objectif de faire comprendre aux étudiants comment écrire une fonction qui retourne un résultat entier. Considérons la fonction qui calcule la valeur absolue d’un entier. Nous avons présenté dans le chapitre précédent quelques tests unitaires qui permettent de vérifier le bon fonctionnement de cette fonction. Il nous reste maintenant à convertir tout cela en un exercice INGInious.
Pour cela, vous pouvez vous appuyer sur un squelette de base d’exercice INGInious en Python. Celui-ci est disponible via le repo GitHub https://github.com/obonaventure/LINFO1002-P1.
Ce repo comprend six fichiers et deux répertoires. Le répertoire $common
contient des utilitaires qui facilitent la création d’exercices INGInious en Python. Vous devez copier ce répertoire et les fichiers Runner.py
et compiler.py
qui s’y trouvent dans le répertoire racine de votre cours INGInious. Pour cela, il vous faudra utiliser WebDav, comme expliqué ci-dessous.
La première étape est de cliquer sur le bouton Course administration. INGInious affiche maintenant la page de gestion du cours qui contient de nombreuses informations sur les responsables du cours, sa disponibilité, des statistiques, etc. Sur la barre de menu, le bouton Tasks permet d’accéder aux différentes tâches associées au cours, pour l’instant encore vide. INGInious vous permet de monter votre cours comme un répertoire sur votre ordinateur. Pour cela, cliquez sur le bouton WebDAV access
se trouvant sur cette page.
WebDAV est un protocole permettant d’accéder à un serveur de fichiers à travers le web. Son fonctionnement sort du cadre de ce projet, mais vous trouverez des informations complémentaires sur la page qui lui est consacrée sur wikipedia: https://en.wikipedia.org/wiki/WebDAV. INGInious vous génère un très long mot de passe unique qui vous permet d’accéder au serveur webdav.
Vous trouverez sur Internet et notamment https://www.webdavsystem.com/server/access/ des informations sur l’utilisation de WebDAV sur votre système d’exploitation préféré. Nous détaillerons ici Windows et Linux. Appuyez sur le bouton WebDav et copiez l’URL donnée. Ouvrez ensuite votre explorateur de fichier. Sur Windows, faites un clic droit sur « Réseau » puis cliquez sur « Connecter un lecteur réseau », comme ceci:
Et sur Linux, cliquez sur « Connexion à un serveur », tout en bas à gauche:
Entrez le nom d’utilisateur et le mot de passe que vous voyez sur INGInious, et vous avez maitenant accès à tous vos fichiers, directement sur votre ordinateur ! Profitez-en pour ajouter le dossier « $common » (attention au $) et son contenu, compiler.py et Runner.py, à la racine de notre cours (là où vous arrivez à la connexion).
Revenons à l’interface en ligne, et concentrons-nous maintenant sur ce qui se trouve sous le bouton WebDav. Pour ajouter une tâche, il faut cliquer sur le menu en hamburger, et sélectionner « Add Task ».
Ensuite, entrez le nom de votre tâche. Attention, celui-ci sert d’ID à votre tâche lors de la création, évitez donc les espaces et les caractères spéciaux. Vous pourrez changer son nom ensuite.
Quand vous avez appuyé sur « Create Task », vous obtenez ceci:
Vous remarquerez que vous ne pouvez pas encore éditer cette nouvelle tâche. Pour ce faire, n’oubliez de valider votre changement en poussant sur « Save changes ». Vous verrez un message de confirmation:
[Remarque]: Si vous faites une erreur dans le processus de création de tâche, il est possible qu’INGInious vous bloque en vous empêchant de la supprimer. Si c’est le cas, ouvrez à nouveau WebDav comme expliqué ci-dessus, et éditez le fichier course.yml en enlevant l’id erronée de l’attribut tasks_list, ainsi que le dossier de la tâche, puis réessayez.
Vous pouvez à présent cliquer sur le crayon bleu « Edit task » pour pouvoir spécifier:
un titre résumant la tâche (champ
Name
);une brève explication sur la tâche (champ
Context
);l’auteur de la tâche (champ
Author
);un URL permettant de contacter l’auteur de la tâche en cas de problème. Cela peut être un URL
mailto
pour l’envoi d’un email ou un URL pointant vers un forum ou un formulaire de création d’issue sur GitHub ou autre;des catégories qui permettent de regrouper les tâches par domaine (nous ne les utiliserons pas);
le poids relatif de la question par rapport aux autres du même cours.
Les autres informations ne nous concernent pas dans le cadre de ce projet et il est inutile de les modifier. N’oubliez pas de pousser sur Save changes
quand vous avez fini de remplir ce formulaire.
Il nous faut maintenant spécifier l’environnement logiciel qui va être utilisé pour vérifier l’exercice en cliquant sur le bouton Environment
. Vous devez sélectionner un Docker container
et pyjavacpp
comme Grading environment
. Les autres paramètres ne doivent pas être modifiés. Vous pouvez sauvegarder vos modifications via Save changes
et ensuite définir vos exercices en cliquant sur Subproblems
.
Commençons par créer une question baptisée q1
et de type code
en cliquant sur le bouton Add
. INGInious supporte d’autres types de questions, mais nous ne les utiliserons pas dans ce projet.
Vous pouvez maintenant agrandir la zone de texte relative à la question q1
et remplir les boîtes de dialogue Name
et Context
ainsi qu’indiquer que le langage Python est utilisé. Les autres champs ne doivent pas être modifiés.
Vous pouvez maintenant créer les fichiers qui vont permettre de tester l’exercice INGInious. Pour cela, cliquez sur Task files
pour visualiser et modifier les fichiers qui correspondent à cette tâche INGInious.
A ce stade, le répertoire INGInious correspondant à la tâche est vide. Nous allons le remplir en nous inspirant du repo GitHub https://github.com/obonaventure/LINFO1002-P1. Vous pouvez ici utiliser à nouveau WebDav, ou le faire directement dans l’interface INGInious. Nous utiliserons l’interface.
Par convention, un exercice INGInious aura la structure suivante:
un fichier
run
qui est exécuté par INGInious pour évaluer la tâche. Prenez le fichier disponible sur GitHub et ne le modifiez pas.un répertoire
src
qui va contenir votre fonction correcte, votre suite de test et un répertoire nomméTemplates
qui est utilisé par le fichierrun
.un répertoire
test
que nous utiliserons par après
Commençons par créer le template src/Templates/abs
qui contient la définition de la fonction et éventuellement des import
dont elle pourrait avoir besoin. Ce fichier doit se trouver dans le sous-répertoire Templates
du répertoire src
.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
def abs(entier):
@ @q1@@
Nous créons d’abord le fichier dans le répertoire racine.
Ensuite il suffit de le déplacer vers src/Templates
.
Ce fichier contient le squelette dans lequel le code que l’étudiant entrera sur INGInious sera ajouté. Le symbole @ @q1@@
est l’identifiant de la question, dans notre cas q1
. Cet identifiant sera remplacé par le code de l’étudiant. Il est possible d’intégrer plusieurs sous-questions dans le même template si nécessaire.
Ensuite nous pouvons créer une version correcte de la fonction demandée aux étudiants. Cette fonction est placée dans le fichier src/CorrAbs.py
qui se trouve dans le répertoire src
.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
def abs(entier):
"""
@pre entier est un nombre entier
@post retourne la valeur absolue de ce nombre
"""
if entier >= 0:
return entier
return -entier
Il suffit maintenant de remplir ce fichier.
Nous pouvons enfin construire les tests unitaires. Ceux-ci se trouveront dans le fichier src/TestAbs.py
que vous pouvez télécharger via src/TestAbs.py
.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import unittest
import sys
import CorrAbs as correct
import abs as student
class TestAbs(unittest.TestCase):
def test0_None(self):
args = [-3, 0, 5]
rep = _("Votre fonction a retourné None pour {} comme argument. Cela implique probablement qu'il manque un return dans votre code.")
for n in args:
try:
student_ans = student.abs(n)
except Exception as e:
self.fail("Votre fonction a provoqué l'exception {}: {} avec comme argument {}".format(type(e), e, n))
self.assertIsNotNone(student_ans, rep.format(n))
def test1_abs_0(self):
args = [0]
rep = _("Votre fonction a retourné {} lorsqu'elle est appelée avec {} comme argument alors que la réponse attendue est {}")
for n in args:
try:
student_ans = student.abs(n)
except Exception as e:
self.fail("Votre fonction a provoqué l'exception {}: {} avec comme argument {}".format(type(e), e, n))
correct_ans = correct.abs(n)
self.assertEqual(student_ans, correct_ans,
rep.format(student_ans, n, correct_ans))
def test1_abs_pos(self):
args = [1, 5, 17, 98]
rep = _("Votre fonction a retourné {} lorsqu'elle est appelée avec {} comme argument positif alors que la réponse attendue est {}")
for n in args:
try:
student_ans = student.abs(n)
except Exception as e:
self.fail("Votre fonction a provoqué l'exception {}: {} avec comme argument {}".format(type(e), e, n))
correct_ans = correct.abs(n)
self.assertEqual(student_ans, correct_ans,
rep.format(student_ans, n, correct_ans))
def test2_abs_neg(self):
args = [-1, -9, -22, -1234]
rep = _("Votre fonction a retourné {} lorsqu'elle est appelée avec {} comme argument négatif alors que la réponse attendue est {}")
for n in args:
try:
student_ans = student.abs(n)
except Exception as e:
self.fail("Votre fonction a provoqué l'exception {}: {} avec comme argument {}".format(type(e), e, n))
correct_ans = correct.abs(n)
self.assertEqual(student_ans, correct_ans,
rep.format(student_ans, n, correct_ans))
if __name__ == '__main__':
unittest.main()
Il y a quelques particularités à remarquer par rapport à la présentation générale des tests unitaires dans le chapitre précédent. Tout d’abord, nous importons deux versions de la fonction à tester :
l’implémentation correcte en utilisant
import CorrAbs as correct
la solution de l’étudiant en utilisant
import abs as student
Ces deux importations nous permettent d’exécuter la fonction écrite par l’étudiant en appelant student.abs()
et la version correcte via correct.abs()
.
Une deuxième différence importante avec les tests unitaires classiques est que vous ne pouvez pas faire d’hypothèse sur le code écrite par les étudiants. Il est très possible que celui-ci contiennent des erreurs qui vont provoquer une exception. Une façon simple est d’entourer l’appel à student.abs()
avec un try: ... except:
comme ci-dessous.
try:
student_ans = student.abs(n)
except Exception as e:
self.fail("Votre fonction a provoqué l'exception {}: {} avec comme argument {}".format(type(e), e, n))
Ce try: ... except:
capture toutes les exceptions possibles. Vous pouvez bien entendu l’améliorer en fournissant aux étudiants un message d’erreur spécifique à l’exception provoquée par vos tests. Pour le calcul de la valeur absolue, le code des étudiants ne devrait normalement pas provoquer d’exception, mais d’autres types d’exercices le pourraient.
Une troisième différence est que unittest
exécute par défaut les tests dans l’ordre alphabétique du nom des méthodes de la classe TestAbs
. Si vous souhaitez que certains tests soient exécutés avant d’autres, choisissez intelligemment les noms de vos méthodes de test.
Vous pouvez maintenant essayer votre premier exercice INGInious en Python et voir comment la suite de test réagit à vos erreurs. N’hésitez pas à modifier la suite de tests pour l’améliorer.
Lors du développement de vos exercices INGInious, il est possible que votre suite de test ne fonctionne pas convenablement et qu’INGInious retourne une erreur. Dans ce cas, il est souvent utile d’accéder à de l’information de debug via le bouton debug
se trouvant à droite de la fenêtre INGInious.
Celui-ci vous permet de visualiser plus d’informations collectées lors de l’exécution de vos tests, dont l’erreur standard notamment.
Comment écrire un exercice INGInious en Python ?¶
La plateforme INGInious que vous utilisez dans le cadre de nombreux cours d’informatique à l”UCLouvain est un bel exemple de l’utilisation de tests unitaires à des fins pédagogiques. Dans le cadre des cours d’informatique, un exercice INGInious prend souvent la forme d’une fonction dont les étudiants doivent écrire le corps sur base des spécifications fournies. Dans certains cas, il peut aussi s’agir du squelette d’un objet pour lequel certaines méthodes ou variables d’instance sont à compléter.
La création d’un exercice INGInious se fait en trois étapes:
Pour la première étape, il faut d’abord délimiter la partie de la matière qui est couverte et les compétences des étudiants. Un exercice qui est proposé durant l’apprentissage ne sera pas écrit de la même façon qu’un exercice de révision qui suppose que l’étudiant a suivi l’ensemble du cours. Dans le cadre du projet, votre cible sera des étudiants qui ont suivi l’ensemble du premier cours d’informatique et se préparent à passer leur premier examen. Vos exercices INGInious devront les aider à renforcer leurs compétences dans le domaine de la programmation en Python.
Pour la deuxième étape, appuyez-vous sur votre propre expérience de l’apprentissage de la programmation et demandez aux autres membres du groupe d’essayer les exercices que vous proposez et d’imaginer des erreurs que les étudiants pourraient faire.
La troisième étape est probablement la plus compliquée. Il faut parvenir à trouver les tests qui permettront d’identifier de la façon la plus précise les erreurs des étudiants. Ce n’est pas toujours possible et cela dépend généralement des arguments que l’on peut passer à la fonction qui doit être implémentée dans l’exercice et de son résultat. Plus la fonction est riche, plus il est facile d’imaginer des tests. À titre d’exemple, considérons la fonction
present
qui permet de tester si un entier est présent dans un tableau passé en argument.Cette fonction retourne naturellement un booléen. On peut la tester sur base de différents tableaux d’entrée, mais il sera difficile de donner un feedback à l’étudiant sur base uniquement de la valeur qu’elle retourne. Si on remplace cette fonction par une fonction qui compte le nombre d’occurrences, cela permet d’avoir des tests plus riches pour lesquels il est plus facile de donner un feedback constructif. C’est encore mieux si la fonction retourne la liste des indices des positions du tableau qui contiennent le nombre passé en argument.