lundi 28 février 2011

Ordonner les tests JUnit

Depuis l'introduction des annotations en JAVA 5 et leur prise en compte par JUnit 4, les projets utilisant les versions antérieures de JUnit peuvent rencontrer quelques petites difficultés pour passer aux annotations. Dans cet article, je vais m'attacher à la problématique suivante : ordonner l'exécution des méthodes de tests au sein d'une classe de test en utilisant les annotations.

Sans les annotations
Je ne vais pas rentrer dans le détail. Internet est très riche en article sur ce sujet. Je rappelle uniquement qu'il fallait implémenter au sein de la classe de test la méthode "suite" de la manière suivante par exemple :


import junit.framework.TestCase;
import junit.framework.TestSuite;


public class MyClassTest extends TestCase {

      …

      public MyClassTest(String testName) {
            super(testName);
            …
      }

      public void testMethod1() {
           
      }
     
      public void testMethod2() {
           
      }

      public static junit.framework.Test suite() {
            TestSuite suite = new TestSuite("Test MyClass");
            suite.addTest(new MyClassTest("testMethod2"));
            suite.addTest(new MyClassTest("testMethod1"));
            return suite;
      }

}

Il faut également savoir que sans la méthode "suite", l'ordre d'exécution des méthodes de test n'est pas prévisible puisqu'elles sont censées être indépendantes les unes des autres.

Avec les annotations
Il existe plusieurs approches pour ordonner les méthodes de test dans un projet utilisant les annotations JAVA. Je vais passer en revue 3 d'entre elles.

Approche n°1
La première approche est la plus simple.
En partant du code ci-dessous par exemple


import static junit.framework.Assert.assertTrue;

import org.junit.Test;


public class MyClass0Test {
           
      @Test
      public void testGetString() {
            MyClass myClass = new MyClass();
            assertTrue("1".equals(myClass.getString()));
      }
     
      @Test
      public void testGetInt() {
            MyClass myClass = new MyClass();
            assertTrue(1 == myClass.getInt());
      }
     
}

on peut aboutir à


import static junit.framework.Assert.assertTrue;

import org.junit.Test;


public class MyClass1Test {

      private MyClass myClass;
     
      public MyClass1Test () {
            myClass = new MyClass();
      }
           
      private void testGetString() {
            assertTrue("1".equals(myClass.getString()));
      }
     
      private void testGetInt() {
            assertTrue(1 == myClass.getInt());
      }

      @Test
      public void testSuite() {
            testGetInt();
            testGetString();
      }
     
}

Les annotations "@Test" sont enlevées, les méthodes de test sont rendues "private" et l'on crée une nouvelle méthode de test "public" précédée de "@Test" dans laquelle on indique l'ordre des méthodes de test "private" à exécuter.

Approche n°2
La seconde approche est un peu plus complexe.
En partant du même code (vu précédemment), on peut aboutir au code suivant :


import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.junit.Test;


public class MyClass2Test extends TestCase {

      private MyClass myClass;
     
      public MyClass2Test(String testName) {
            super(testName);
            myClass = new MyClass();
      }
     
      @Test
      public void testGetString() {
            assertTrue("1".equals(myClass.getString()));
      }
     
      @Test
      public void testGetInt() {
            assertTrue(1 == myClass.getInt());
      }

      public static junit.framework.Test suite() {
            TestSuite suite = new TestSuite("Test MyClass");
            suite.addTest(new MyClass2Test("testGetInt"));
            suite.addTest(new MyClass2Test("testGetString"));
            return suite;
      }

}

Notre classe de test hérite de la classe "TestCase". Elle possède un constructeur et un seul avec un paramètre représentant la méthode à exécuter. Elle possède également un champ "private" instance de la classe à tester. Et elle implémente la fameuse méthode statique "suite".

Approche n°3
La troisième approche est la plus complexe. Elle suppose que l'on se trouve dans un cas où l'on ne peut pas hériter directement de la classe "TestCase".
En partant du même code (vu dans l'approche n°1), on peut aboutir au code suivant :


import static junit.framework.Assert.assertTrue;
import junit.framework.TestSuite;

import org.junit.Test;


public class MyClass3Test extends AbstractBaseTest {

      private MyClass myClass;
           
      public MyClass3Test() {
            myClass = new MyClass();
      }
     
      @Test
      public void testGetString() {
            assertTrue("1".equals(myClass.getString()));
      }
     
      @Test
      public void testGetInt() {
            assertTrue(1 == myClass.getInt());
      }
     
      public static junit.framework.Test suite() {
            TestSuite suite = new TestSuite("Test MyClass");
            suite.addTest(createTestCase(new MyClass3Test(), "testGetInt"));
            suite.addTest(createTestCase(new MyClass3Test(), "testGetString"));
            return suite;
      }

}

Notre classe de test "MyClass3Test" hérite de la classe abstraite "AbstractBaseTest".

import junit.framework.TestCase;


public abstract class AbstractBaseTest {

      protected static TestCase createTestCase(AbstractBaseTest mytest, String testName) {
            TestCase test = new MyTestCase(testName, mytest);
            return test;
      }

}

Cette classe abstraite possède une méthode statique permettant de créer une instance de "MyTestCase" pour une classe de test donnée et pour une méthode de test donnée.

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import junit.framework.TestCase;


public class MyTestCase extends TestCase {

      private String fName;
      private AbstractBaseTest mytest;
     
      public MyTestCase(String testName, AbstractBaseTest mytest) {
            super(testName);
            this.mytest = mytest;
            this.fName = testName;
      }

      @Override
      protected void runTest() throws Throwable {
            assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null);
            Method runMethod= null;
            try {
                  // use getMethod to get all public inherited
                  // methods. getDeclaredMethods returns all
                  // methods of this class but excludes the
                  // inherited ones.
                  runMethod= mytest.getClass().getMethod(fName, (Class[])null);
            } catch (NoSuchMethodException e) {
                  fail("Method \""+fName+"\" not found");
            }
            if (!Modifier.isPublic(runMethod.getModifiers())) {
                  fail("Method \""+fName+"\" should be public");
            }

            try {
                  runMethod.invoke(mytest);
            }
            catch (InvocationTargetException e) {
                  e.fillInStackTrace();
                  throw e.getTargetException();
            }
            catch (IllegalAccessException e) {
                  e.fillInStackTrace();
                  throw e;
            }          
      }
     
     
}
 
Cette classe "MyTestCase" hérite de "TestCase" et surcharge la méthode "runTest". En réalité, on reprend ici l'implémentation de "runTest" effectuée au niveau de "TestCase" en l'adaptant à notre besoin.

On peut ajouter pour exemple la classe lançant tous les tests :


import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses( { MyClass0Test.class, MyClass1Test.class, MyClass2Test.class, MyClass3Test.class } )
public class AllTest {

}

Conclusion
Nous avons parcouru 3 manières de traiter l'ordonnancement de l'exécution des méthodes de test. Il en existe d'autres. Et surtout, il est possible d'utiliser des frameworks adaptés tel que TestNG.

Aucun commentaire:

Enregistrer un commentaire