Comment tester une classe abstraite en Java avec JUnit?

87

Je suis nouveau dans les tests Java avec JUnit. Je dois travailler avec Java et j'aimerais utiliser des tests unitaires.

Mon problème est: j'ai une classe abstraite avec des méthodes abstraites. Mais il existe des méthodes qui ne sont pas abstraites. Comment puis-je tester cette classe avec JUnit? Exemple de code (très simple):

abstract class Car {

    public Car(int speed, int fuel) {
        this.speed = speed;
        this.fuel = fuel;
    }

    private int speed;
    private int fuel;

    abstract void drive();

    public int getSpeed() {
        return this.speed;
    }

    public int getFuel() {
        return this.fuel;
    }
}

Je veux tester getSpeed()et getFuel()fonctionner.

Une question similaire à ce problème est ici , mais elle n'utilise pas JUnit.

Dans la section FAQ de JUnit, j'ai trouvé ce lien , mais je ne comprends pas ce que l'auteur veut dire avec cet exemple. Que signifie cette ligne de code?

public abstract Source getSource() ;
Vasco
la source
4
Voir stackoverflow.com/questions/1087339/… pour deux solutions utilisant Mockito.
ddso le
Y a-t-il un avantage à apprendre un autre cadre de test? Mockito est-il seulement une extension de jUnit, ou un projet complètement différent?
vasco le
Mockito ne remplace pas JUnit. Comme d'autres frameworks moqueurs, il est utilisé en plus d'un framework de test unitaire et vous aide à créer des objets simulés à utiliser dans vos cas de test.
ddso le
1
Indépendant de la

Réponses:

104

Si vous n'avez pas d'implémentations concrètes de la classe et que les méthodes ne servent pas à staticles tester? Si vous avez une classe concrète, vous testerez ces méthodes dans le cadre de l'API publique de la classe concrète.

Je sais ce que vous pensez "Je ne veux pas tester ces méthodes encore et encore, c'est la raison pour laquelle j'ai créé la classe abstraite", mais mon contre-argument est que le but des tests unitaires est de permettre aux développeurs d'apporter des modifications, exécutez les tests et analysez les résultats. Une partie de ces changements pourrait inclure le remplacement des méthodes de votre classe abstraite, à la fois protectedet public, ce qui pourrait entraîner des changements de comportement fondamentaux. En fonction de la nature de ces modifications, cela peut affecter la manière dont votre application s'exécute de manière inattendue, voire négative. Si vous avez une bonne suite de tests unitaires, les problèmes résultant de ces types de modifications devraient être apparents au moment du développement.

nsfyn55
la source
17
La couverture de code à 100% est un mythe. Vous devez avoir exactement suffisamment de tests pour couvrir toutes vos hypothèses connues sur le comportement de votre application (écrites de préférence avant d'écrire le code de développement piloté par les tests). Je travaille actuellement sur une équipe TDD très performante et nous n'avons qu'une couverture de 63% à partir de notre dernière version, tout écrite au fur et à mesure de notre développement. Est-ce bon? qui sait?, mais je considérerais que c'est une perte de temps de revenir en arrière et d'essayer d'augmenter cela plus haut.
nsfyn55 du
3
sûr. Certains diront que c'est une violation du bon TDD. Imaginez que vous faites partie d'une équipe. Vous faites l'hypothèse que la méthode est finale et ne mettez pas de tests pour des implémentations concrètes. Quelqu'un supprime le modificateur et apporte des modifications qui se répercutent sur une branche entière de la hiérarchie d'héritage. Ne voudriez-vous pas que votre suite de tests comprenne cela?
nsfyn55
31
Je ne suis pas d'accord. Que vous travailliez dans TDD ou non, la méthode concrète de votre classe abstraite contient du code, par conséquent, ils devraient avoir des tests (qu'il y ait ou non des sous-classes). De plus, les tests unitaires dans les classes Java testent (normalement). Par conséquent, il n'y a vraiment aucune logique à tester des méthodes qui ne font pas partie de la classe, mais plutôt de sa super classe. Suivant cette logique, nous ne devons tester aucune classe en Java, sauf pour les classes sans sous-classes du tout. En ce qui concerne les méthodes remplacées, c'est exactement à ce moment que vous ajoutez un test pour vérifier les modifications / ajouts au test de la sous-classe.
ethanfar le
3
@ nsfyn55 Et si les méthodes concrètes l'étaient final? Je ne vois pas de raison de tester la même méthode plusieurs fois si la mise en œuvre ne peut pas changer
Dioxin
3
Ne devrions-nous pas demander aux tests de cibler l'interface abstraite afin de pouvoir les exécuter pour toutes les implémentations? Si ce n'est pas possible, nous violerions celui de Liskov, que nous voudrions connaître et corriger. Ce n'est que si l'implémentation ajoute des fonctionnalités étendues (compatibles) que nous devrions avoir un test unitaire spécifique pour cela (et juste pour cette fonctionnalité supplémentaire).
tne le
36

Créez une classe concrète qui hérite de la classe abstraite, puis testez les fonctions que la classe concrète hérite de la classe abstraite.

Kevin Bowersox
la source
Que feriez-vous dans le cas où vous auriez 10 classes concrètes étendant la classe abstraite et que chacune de ces classes concrètes implémenterait une seule méthode et disons que les 2 autres méthodes sont les mêmes pour chacune de ces classes, car elles sont implémentées dans l'abstrait classe? Mon cas est que je ne souhaite pas copier-coller les tests de classe abstraite dans chaque sous-classe.
scarface
12

Avec l'exemple de classe que vous avez posté, il ne semble pas logique de tester getFuel()etgetSpeed() puisqu'ils ne peuvent renvoyer que 0 (il n'y a pas de setters).

Cependant, en supposant qu'il ne s'agissait que d'un exemple simplifié à des fins d'illustration, et que vous ayez des raisons légitimes de tester des méthodes dans la classe de base abstraite (d'autres ont déjà souligné les implications), vous pouvez configurer votre code de test de sorte qu'il crée un sous-classe de la classe de base qui fournit simplement des implémentations factices (sans opération) pour les méthodes abstraites.

Par exemple, dans votre, TestCasevous pouvez faire ceci:

c = new Car() {
       void drive() { };
   };

Ensuite, testez le reste des méthodes, par exemple:

public class CarTest extends TestCase
{
    private Car c;

    public void setUp()
    {
        c = new Car() {
            void drive() { };
        };
    }

    public void testGetFuel() 
    {
        assertEquals(c.getFuel(), 0);
    }

    [...]
}

(Cet exemple est basé sur la syntaxe JUnit3. Pour JUnit4, le code serait légèrement différent, mais l'idée est la même.)

Grodriguez
la source
Merci de répondre. Oui, mon exemple a été simplifié (et pas si bon). Après avoir lu toutes les réponses ici, j'ai écrit une classe factice. Mais comme l'écrit @ nsfyn55 dans sa réponse, j'écris un test pour chaque descendant de cette classe abstraite.
vasco
9

Si vous avez quand même besoin d'une solution (par exemple parce que vous avez trop d'implémentations de la classe abstraite et que les tests répéteraient toujours les mêmes procédures), vous pouvez créer une classe de test abstraite avec une méthode de fabrique abstraite qui sera exécutée par l'implémentation de cette classe de test. Cet exemple fonctionne ou moi avec TestNG:

La classe de test abstraite de Car:

abstract class CarTest {

// the factory method
abstract Car createCar(int speed, int fuel);

// all test methods need to make use of the factory method to create the instance of a car
@Test
public void testGetSpeed() {
    Car car = createCar(33, 44);
    assertEquals(car.getSpeed(), 33);
    ...

Implémentation de Car

class ElectricCar extends Car {

    private final int batteryCapacity;

    public ElectricCar(int speed, int fuel, int batteryCapacity) {
        super(speed, fuel);
        this.batteryCapacity = batteryCapacity;
    }

    ...

Classe ElectricCarTestde test unitaire de la classe ElectricCar:

class ElectricCarTest extends CarTest {

    // implementation of the abstract factory method
    Car createCar(int speed, int fuel) {
        return new ElectricCar(speed, fuel, 0);
    }

    // here you cann add specific test methods
    ...
thomas.mc.work
la source
5

Tu pourrais faire quelque chose comme ça

public abstract MyAbstractClass {

    @Autowire
    private MyMock myMock;        

    protected String sayHello() {
            return myMock.getHello() + ", " + getName();
    }

    public abstract String getName();
}

// this is your JUnit test
public class MyAbstractClassTest extends MyAbstractClass {

    @Mock
    private MyMock myMock;

    @InjectMocks
    private MyAbstractClass thiz = this;

    private String myName = null;

    @Override
    public String getName() {
        return myName;
    }

    @Test
    public void testSayHello() {
        myName = "Johnny"
        when(myMock.getHello()).thenReturn("Hello");
        String result = sayHello();
        assertEquals("Hello, Johnny", result);
    }
}
iil
la source
4

Je créerais une classe interne jUnit qui hérite de la classe abstraite. Cela peut être instancié et avoir accès à toutes les méthodes définies dans la classe abstraite.

public class AbstractClassTest {
   public void testMethod() {
   ...
   }
}


class ConcreteClass extends AbstractClass {

}
marting
la source
3
C'est un excellent conseil. Il pourrait cependant être amélioré en fournissant un exemple. Peut-être un exemple de la classe que vous décrivez.
SDJMcHattie
2

Vous pouvez instancier une classe anonyme, puis tester cette classe.

public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    private MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.myDependencyService = new MyDependencyService();
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {    
            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Gardez à l'esprit que la visibilité doit protectedconcerner la propriété myDependencyServicede la classe abstraite ClassUnderTest.

Vous pouvez également combiner parfaitement cette approche avec Mockito. Regardez ici .

Samuel
la source
2

Ma façon de tester cela est assez simple, dans chacun d'eux abstractUnitTest.java. Je crée simplement une classe dans abstractUnitTest.java qui étend la classe abstraite. Et testez-le de cette façon.

Haomin
la source
0

Vous ne pouvez pas tester toute la classe abstraite. Dans ce cas, vous avez des méthodes abstraites, cela signifie qu'elles doivent être implémentées par une classe qui étend une classe abstraite donnée.

Dans cette classe, le programmeur doit écrire le code source qui est dédié à sa logique.

En d'autres termes, il n'y a aucun sens de tester une classe abstraite parce que vous n'êtes pas en mesure d'en vérifier le comportement final.

Si vous avez des fonctionnalités majeures non liées aux méthodes abstraites dans une classe abstraite, créez simplement une autre classe où la méthode abstraite lèvera une exception.

Damian Leszczyński - Vash
la source
0

En option, vous pouvez créer une classe de test abstraite couvrant la logique à l'intérieur d'une classe abstraite et l'étendre pour chaque test de sous-classe. Ainsi, vous pouvez vous assurer que cette logique sera testée séparément pour chaque enfant.

Andrew Taran
la source