-
Notifications
You must be signed in to change notification settings - Fork 2
Suivez le guide
PAS A JOUR
Imaginez que vous soyez dans cet état d'esprit :
"J'ai participé au cours LFSAB1401 et j'ai trouvé stimulante la réalisation des exercices en lignes sur le site du cours ou encore durant mon apprentissage personnel je suis tombé sur des colles que je trouverais intéressant de transformer en exercices pour la postérité."
Cela peut être réalisé en suivant quelques étapes simples.
###Je rédige la question
Par exemple,
"Écrivez la signature et le corps d’une méthode baptisée afficheMax qui prend deux nombres entiers comme arguments et affiche le plus grand de ces nombres. La spécification de cette méthode est :"
/**
∗ @pre -
∗ @post affiche le maximum entre les nombres entiers a et b
*
∗ /
###J'écris la réponse à la question
L'idéal serait que je connaisse tout de même déja la réponse à la question que je souhaite poser :)
Je me sert de BlueJ pour écrire le code java qui correspond à la réponse correcte. J'encapsule ma méthode dans une classe nommée MonExoCorr:
package student;
/**
* Écrivez la signature et le corps d'une méthode baptisée
* afficheMax qui prend deux nombres entiers comme arguments
* et affiche le plus grand de ces nombres.
*
* @auteur (pénom nom)
* @version (version 1)
*/
public class MonExoCorr
{
/**
* @pre -
* @post affiche le maximum entre les nombres entiers a et b
*/
public void afficheMax(int a, int b)
{
System.out.println(Math.max( a,b));
}
}
J'écris aussi une réplique de la classe MonExoCorr nommée MonExoStu qui outre le nom contient exactement le même code:
package student;
/**
* Écrivez la signature et le corps d’une méthode baptisée
* afficheMax qui prend deux nombres entiers comme arguments
* et affiche le plus grand de ces nombres.
*
* @auteur (votre nom)
* @version (numéro de version ou date)
*/
public class MonExoStu
{
/**
* @pre -
* @post affiche le maximum entre les nombres entiers a et b
*/
public void afficheMax(int a, int b)
{
System.out.println(Math.max( b,b));
}
}
###Je prends un café et je pense à ce qu'il faut tester
Un bon entraînement que permet l'écriture de tests est "imaginer" le comportement d'un bout de code informatique dans différentes situations et déterminer lesquelles ce ces situations seraient les plus intéressantes à vérifier. C'est une compétence à part entière.
J'ai identifié trois situations lorsque une méthode doit calculer le maximum de deux entiers donnés:
- les deux entiers sont égaux
- le premier est plus grand que le second
- le second est plus grand que le premier
Je ne trouve aucun entier en particulier qui déroge à l'une de ces situations: les paramètres dans mon cas ne peuvent pas être égaux à null
et toute valeur entière tombera dans l'un de ces trois cas.
###J'écris les classes de tests qui vont permettre de vérifier les résultats.
A force d'écrire des tests les programmeurs en sont venus à la conclusion qu'il serait très utile d'avoir un outil spécifique pour cela. Je décide de leur faire confiance :)
L'emploi du framework JUnit à été vu en fin de quadrimestre.
La première classe de test que j'écris ressemble a priori à ceci :
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Écrivez la signature et le corps d’une méthode baptisée
* afficheMax qui prend deux nombres entiers comme arguments
* et affiche le plus grand de ces nombres.
*
* @auteur (prénom nom)
* @version (version 1)
*/
public class MonExoTests extends junit.framework.TestCase
{
//varibles de classe
private int a ;
private int b ;
private String rep1;
private String rep2;
private Random gen = new Random();
private MonExoStu monExoStu;
private MonExoCorr monExoCorr;
/**
* Constructeur par défaut de la classe MonExoTests
*/
public MonExoTests()
{
monExoStu = new MonExoStu();
monExoCorr = new MonExoCorr();
}
/**
* Préparations préalables au test
*
* Appelée avant chaque méthode de la classe
*/
public void setUp()
{
}
/**
* Opérations pour terminer les tests
*
* Appelée après chaque méthode de la classe
*/
public void tearDown()
{
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenAEqualsB()
{
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenAGreaterThanB()
{
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenASmallerThanB()
{
}
}
J'ai écrit le squelette d'un TestCase
avec trois tests qui représentent indépendamment trois situations dans lesquelles la question de mon énoncé pourrait être posée.
J'aurais pu choisir plusieurs autres manières d'écrire ma classe. Par exemple, ne choisir qu'une seule fois a
et b
au hasard pour les trois cas, mettre les trois cas dans une seule méthode de test, faire deux méthodes de test au lieu de trois...
Lorsqu'on écrit des tests il est normal d'avoir parfois de longs noms de méthodes car lorsque seules les signatures de méthodes sont affichées à l'utilisateur il faut pouvoir donner un maximum d'informations sur ce que cette méthode a trouvé comme erreur ( dans une situation "normale" de programmation cela serait plutôt un piètre choix de conception).
Concernant les attributs de la classe, l'objet de type Random
va m'aider à rendre les tests plus "vivants" en produisant des valeurs aléatoires cela n'est pas indispensable mais aide à réduire la probabilité que par exemple l'étudiant devine les valeurs de test ou les code en dur par erreur dans son propre code.
Les paramètres a
et b
à passer à la méthode qui est testée, les objets monExoStu
monExoCorr
qui contiennent la réponse de l'étudiant et le corrigé de l'exercice ainsi que des variables rep1
et rep2
qui contiendront les valeurs imprimées sont aussi des attributs de la classe.
Il va me falloir ensuite mettre un contenu à mes méthodes de test. En simple, ce que je voudrais faire c'est ceci:
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Random;
/**
* Écrivez la signature et le corps d’une méthode baptisée
* afficheMax qui prend deux nombres entiers comme arguments
* et affiche le plus grand de ces nombres.
*
* @auteur (votre nom)
* @version (la version ou une date)
*/
public class MonExoTests extends junit.framework.TestCase
{
//variables de classe
private int a ;
private int b ;
private String rep1;
private String rep2;
private Random gen = new Random();
private MonExoStu monExoStu;
private MonExoCorr monExoCorr;
private PrintStream old = System.out;
private ByteArrayOutputStream baos1;
private ByteArrayOutputStream baos2;
private PrintStream ps1;
private PrintStream ps2;
/**
* Constructeur par défaut de la classe MonExoTests
*/
public MonExoTests()
{
monExoStu = new MonExoStu();
monExoCorr = new MonExoCorr();
}
/**
* Préparations préalables au test
*
* Appelée avant chaque méthode de la classe
*/
public void setUp()
{
}
/**
* Opérations pour terminer les tests
*
* Appelée après chaque méthode de la classe
*/
public void tearDown()
{
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenAEqualsB()
{
//initialisation des valeurs de test
a = gen.nextInt();
b = a;
//lancement d'une routine de test qui vérifie
//les réponses imprimées en sortie standard
assertPrintsRightAnswer();
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenAGreaterThanB()
{
//initialisation des valeurs de test
a = gen.nextInt();
while(a <= b){
b = gen.nextInt();
}
//lancement d'une routine de test qui vérifie
//les réponses imprimées en sortie standard
assertPrintsRightAnswer();
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenASmallerThanB()
{
//initialisation des valeurs de test
a = gen.nextInt();
while(a >= b){
b = gen.nextInt();
}
//lancement d'une routine de test qui vérifie
//les réponses imprimées en sortie standard
assertPrintsRightAnswer();
}
}
Je voudrais vérifier que le code de l'étudiant d'une part et le code du corrigé d'autre part impriment bien la même réponse à l'écran en me servant des assertions de JUnit.
La méthode nextInt()
est l'une des méthodes rendues disponibles par le générateur pour créer des valeurs entières aléatoires. Je génère donc des valeurs jusqu'à ce qu'elles respectent mes spécifications pour a
et b
.
Cependant, ma classe test ne peut pas encore être exécutée car en vrai une méthode assertPrintsRightAnswer()
n'existe pas. Toutefois comme vous aurez pu le remarquer, en programmation, tout ce dont on a besoin et qui n'existe pas n'attend que d'être crée :
/**
* @pre -
* @post -
*/
private void assertPrintsRightAnswer()
{
//initialisations
baos1 = new ByteArrayOutputStream();
baos2 = new ByteArrayOutputStream();
ps1 = new PrintStream(baos1);
ps2 = new PrintStream(baos2);
// sauvegarder le flux normal
PrintStream old = System.out;
// dire à java qu'il faut utiliser mon flux
System.setOut(ps1);
// exécution du code correct
monExoCorr.afficheMax(a, b);
//changer à nouveau le flux
System.out.flush();
System.setOut(ps2);
try{
monExoStu.afficheMax(a, b);
}
catch(Throwable e){
fail("Vous effectuez une opération "
+ "illégale dans votre code: "+e.getMessage());
}
// Remettre les choses en place
System.out.flush();
System.setOut(old);
//Récupérerles réponses
rep1 = baos1.toString();
rep2 = baos2.toString();
//effectuer la vérifiaction
assertTrue("Votre réponse n'est pas la réponse attendue."
+ "\nVotre réponse: "+rep2+
"\nLa réponse attendue : "+rep1, rep1.equals(rep2));
}
Cette méthode auxiliaire détourne les Strings que l'exécution du code des deux exercices affiche à l'écran et les met dans les variables rep1
et rep2
. Elle compare ensuite à l'aide de la méthode equals()
le contenu des deux String et lance une exception de type AssertionError
si le contenu n'est pas le même. Vous ne devez pas comprendre cette méthode en détails jusqu'à l'appel de la méthode asssertTrue()
. Les lignes les plus importantes sont:
monExoCorr.afficheMax(a, b);
monExoStu.afficheMax(a, b);
fail("Vous effectuez une opération illégale dans votre code: "+e.getMessage());
-
assertTrue("Votre réponse n'est pas la réponse attendue. \nVotre réponse: "+rep2+ "\nLa réponse attendue : "+rep1, rep1.equals(rep2));
La section JUnit vous parle d'autres méthodes d'assertions utilisables.
Le rendu final de ma classe est le suivant:
package student;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Random;
/**
* Écrivez la signature et le corps d’une méthode baptisée
* afficheMax qui prend deux nombres entiers comme arguments
* et affiche le plus grand de ces nombres.
*
* @auteur (prénom nom)
* @version (version 1)
*/
public class MonExoTests extends junit.framework.TestCase
{
//variables de classe
private int a ;
private int b ;
private String rep1;
private String rep2;
private Random gen = new Random();
private MonExoStu monExoStu;
private MonExoCorr monExoCorr;
private PrintStream old = System.out;
private ByteArrayOutputStream baos1;
private ByteArrayOutputStream baos2;
private PrintStream ps1;
private PrintStream ps2;
/**
* Constructeur par défaut de la classe MonExoTests
*/
public MonExoTests()
{
monExoStu = new MonExoStu();
monExoCorr = new MonExoCorr();
}
/**
* Préparations préalables au test
*
* Appelée avant chaque méthode de la classe
*/
public void setUp()
{
baos1 = new ByteArrayOutputStream();
baos2 = new ByteArrayOutputStream();
ps1 = new PrintStream(baos1);
ps2 = new PrintStream(baos2);
}
/**
* Opérations pour terminer les tests
*
* Appelée après chaque méthode de la classe
*/
public void tearDown()
{
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenAEqualsB()
{
//initialisation des valeurs de test
a = gen.nextInt();
b = a;
//lancement d'une routine de test qui vérifie
//les réponses imprimées en sortie standard
assertPrintsRightAnswer();
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenAGreaterThanB()
{
//initialisation des valeurs de test
a = gen.nextInt();
while(a <= b){
b = gen.nextInt();
}
//lancement d'une routine de test qui vérifie
//les réponses imprimées en sortie standard
assertPrintsRightAnswer();
}
/**
* @pre gen est initialisé
* @post -
*/
public void testCorrectnessWhenASmallerThanB()
{
//initialisation des valeurs de test
a = gen.nextInt();
while(a >= b){
b = gen.nextInt();
}
//lancement d'une routine de test qui vérifie
//les réponses imprimées en sortie standard
assertPrintsRightAnswer();
}
/**
* @pre -
* @post -
*/
private void assertPrintsRightAnswer()
{
// sauvegarder le flux normal
old = System.out;
// dire à java qu'il faut utiliser mon flux
System.setOut(ps1);
// exécution du code correct
monExoCorr.afficheMax(a, b);
//changer à nouveau le flux
System.out.flush();
System.setOut(ps2);
try{
monExoStu.afficheMax(a, b);
}
catch(Throwable e){
fail("Vous effectuez une opération "
+ "illégale dans votre code: "+e.getMessage());
}
// Remettre les choses en place
System.out.flush();
System.setOut(old);
//Récupérerles réponses
rep1 = baos1.toString();
rep2 = baos2.toString();
//effectuer la vérifiaction
assertTrue("Votre réponse n'est pas la réponse attendue."
+ "\nVotre réponse: "+rep2+
"\nLa réponse attendue : "+rep1, rep1.equals(rep2));
}
}
###Je teste mes tests
Sur BlueJ, je teste mes tests afin d'être sûr qu'ils se comportent comme je le veux. J'introduis délibérément des erreurs...
En utilisant les fonctionnalités de BlueJ afin d'écrire ces tests, BlueJ s'est chargé lui-même de mettre en place le reste des classes nécessaires.
Afin que mes tests deviennent plus portables, une section de ce manuel est dédié à montrer comment écrire le reste des classes requises en expliquant les conventions dont nous nous sommes servis pour cela.
Dans mon cas, il faudra juste une classe MonExo
contenant la méthode main
qui fera tourner les tests et qui récupérera les résultats:
package student;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Random;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.runner.notification.Failure;
import org.junit.runners.Suite;
/**
* Écrivez la signature et le corps d’une méthode baptisée
* afficheMax qui prend deux nombres entiers comme arguments
* et affiche le plus grand de ces nombres.
*
* @auteur (votre nom)
* @version (la version ou une date)
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({
/**
* Mettre ici la liste des classes tests
* qui constituent la TesteSuite.
*/
MonExoTests.class
})
public class MonExo
{
public static void main(String[] args)
{
Result result = JUnitCore.runClasses(MonExoTests.class);
Iterator<Failure> failures = result.getFailures().iterator();
Failure f;
while(failures.hasNext()){
f = failures.next();
System.err.println(f.getMessage());
}
if(result.wasSuccessful() == true){
//127 : nombre magique afin de signaler
// que tout les tests sont passés.
System.exit(127);
}
}
}
Vous remarquerez que grâce à l'extension de la classe junit.framework.TestCase
,
je n'ai pas besoin de me servir de la classe Assert
ni de mettre entre autres les annotations @Test
au dessus des signatures des méthodes comme fait dans la section d'explication détaillée de JUnit. Les méthodes setUp()
et tearDown()
sont facultatives dans tous les cas.
Un autre exemple de comment écrire un test pour un exercice donné se trouve dans la section disponible ici.
###J'adapte mon script
Je me rends ici afin de comprendre comment intégrer tout cela à Inginious qui utilise un script bash pour faire tourner les tests. La bonne nouvelles c'est que la (presque) totalité du code m'est déjà fournie ! :). Tout ce que je dois faire c'est changer quelques variables du template (modèle) disponible ici au début du fichier.
EXERCICE="MonExo"
CUSTOMSCRIPT="sh custom.sh"
CORR=1
# EXECCUSTOM vaut 0 si on n'exécute pas de script "custom" pour faire des vérifications supplémentaires
EXECCUSTOM=0
NEXERCICES=1
###Je découvre une façon simple et intuitive de structurer un texte.
Pour que INGInious comprenne comment vous voudriez afficher vos exerces, il faut lui fournir du YAML. Je peux consulter la section Rédiger l'énoncé de l'exercice pour mieux comprendre de quoi il s'agit (Cette section explique également comment écrire des QCMs dans un fichier yaml compréhensible par INGInious).
Mon énoncé du début se traduit donc comme suit, je mets le tout dans un fichier nommé task.yaml.
accessible: true
author: 'Prénom Nom'
context: |-
Écrivez **la signature** et **le corps** d’une méthode baptisée
``afficheMax`` qui prendre deux nombres entiers comme arguments et
affiche le plus grand de ces nombres. La spécification de cette
méthode est :
.. code-block :: java
/∗∗
∗ @pre -
∗ @post affiche le maximum entre les nombres entiers a et b
∗ /
environment: java7
limits:
memory: '100'
time: '30'
output: '2'
name: 'Titre de Mon Exercice'
problems:
q1:
language: java
name: ''
type: code
header: ''
weight: 1.0
Une petite modification de mon fichier MonExoStu.java sera nécessaire avant de conclure. Elle est expliquée ici et va permettre à INGInious d'insérer la réponse de l'étudiant au bon endroit dans la classe MonExoStu.
Les fichiers que j'aurais écrits à sauvegarder dans le présent répertoire github sont:
- student/MonExo.java : méthode main...
- student/MonExoVide.java : classe formatée pour INGInious
- student/MonExoCorr.java : le corrigé
- run : script bash
- task.yaml : énoncé formaté pour INGInious
N'hésitez-pas à parcourir le reste du wiki pour plus d'informations! :D