Meilleure pratique: initialiser les champs de classe JUnit dans setUp () ou à la déclaration?

120

Dois-je initialiser les champs de classe lors de la déclaration comme ceci?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Ou dans setUp () comme ça?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

J'ai tendance à utiliser le premier formulaire car il est plus concis et me permet d'utiliser les champs finaux. Si je n'ai pas besoin d'utiliser la méthode setUp () pour la configuration, dois-je quand même l'utiliser et pourquoi?

Clarification: JUnit instanciera la classe de test une fois par méthode de test. Cela signifie que listsera créé une fois par test, quel que soit l'endroit où je le déclare. Cela signifie également qu'il n'y a pas de dépendances temporelles entre les tests. Il semble donc qu'il n'y ait aucun avantage à utiliser setUp (). Cependant, la FAQ JUnit a de nombreux exemples qui initialisent une collection vide dans setUp (), donc je pense qu'il doit y avoir une raison.

Craig P. Motlin
la source
2
Attention, la réponse diffère dans JUnit 4 (initialiser dans la déclaration) et JUnit 3 (utiliser setUp); c'est la racine de la confusion.
Nils von Barth
Voir aussi stackoverflow.com/questions/6094081/…
Grigory Kislin

Réponses:

99

Si vous vous interrogez spécifiquement sur les exemples de la FAQ JUnit, tels que le modèle de test de base , je pense que la meilleure pratique présentée ici est que la classe testée doit être instanciée dans votre méthode setUp (ou dans une méthode de test) .

Lorsque les exemples JUnit créent un ArrayList dans la méthode setUp, ils testent tous le comportement de cet ArrayList, avec des cas tels que testIndexOutOfBoundException, testEmptyCollection, etc. La perspective de quelqu'un qui écrit un cours et s'assure que cela fonctionne correctement.

Vous devriez probablement faire la même chose lorsque vous testez vos propres classes: créez votre objet dans setUp ou dans une méthode de test, afin que vous puissiez obtenir un résultat raisonnable si vous le cassez plus tard.

D'un autre côté, si vous utilisez une classe de collection Java (ou une autre classe de bibliothèque, d'ailleurs) dans votre code de test, ce n'est probablement pas parce que vous voulez le tester - c'est juste une partie du montage de test. Dans ce cas, vous pouvez supposer que cela fonctionne comme prévu, donc l'initialiser dans la déclaration ne sera pas un problème.

Pour ce que ça vaut, je travaille sur une base de code assez volumineuse, vieille de plusieurs années, développée par TDD. Nous initialisons habituellement les choses dans leurs déclarations en code de test, et depuis un an et demi que je suis sur ce projet, cela n'a jamais posé de problème. Il y a donc au moins des preuves anecdotiques que c'est une chose raisonnable à faire.

Moss Collum
la source
45

J'ai commencé à creuser moi-même et j'ai trouvé un avantage potentiel à utiliser setUp(). Si des exceptions sont levées pendant l'exécution de setUp(), JUnit imprimera une trace de pile très utile. D'un autre côté, si une exception est levée pendant la construction de l'objet, le message d'erreur indique simplement que JUnit n'a pas pu instancier le scénario de test et vous ne voyez pas le numéro de ligne où l'échec s'est produit, probablement parce que JUnit utilise la réflexion pour instancier le test Des classes.

Rien de tout cela ne s'applique à l'exemple de création d'une collection vide, car cela ne sera jamais lancé, mais c'est un avantage de la setUp()méthode.

Craig P. Motlin
la source
18

En plus de la réponse d'Alex B.

Il est même nécessaire d'utiliser la méthode setUp pour instancier des ressources dans un certain état. Faire cela dans le constructeur n'est pas seulement une question de minutage, mais en raison de la façon dont JUnit exécute les tests, chaque état de test serait effacé après en avoir exécuté un.

JUnit crée d'abord des instances de testClass pour chaque méthode de test et commence à exécuter les tests après la création de chaque instance. Avant d'exécuter la méthode de test, sa méthode de configuration est exécutée, dans laquelle un état peut être préparé.

Si l'état de la base de données était créé dans le constructeur, toutes les instances instancieraient l'état de la base de données l'une après l'autre, avant d'exécuter chaque test. À partir du deuxième test, les tests s'exécuteraient avec un état incorrect.

Cycle de vie JUnits:

  1. Créez une instance de classe de test différente pour chaque méthode de test
  2. Répétez pour chaque instance de testclass: appelez setup + appelez la méthode de test

Avec quelques enregistrements dans un test avec deux méthodes de test, vous obtenez: (number est le hashcode)

  • Création d'une nouvelle instance: 5718203
  • Création d'une nouvelle instance: 5947506
  • Configuration: 5718203
  • TestOne: 5718203
  • Configuration: 5947506
  • TestTwo: 5947506
Jurgen Hannaert
la source
3
Correct, mais hors sujet. La base de données est essentiellement un état global. Ce n'est pas un problème auquel je suis confronté. Je suis simplement préoccupé par la vitesse d'exécution de tests correctement indépendants.
Craig P. Motlin
Cet ordre d'initialisation n'est vrai que dans JUnit 3, où il s'agit d'une mise en garde importante. Dans JUnit 4, les instances de test sont créées paresseusement, donc l'initialisation dans la déclaration ou dans une méthode de configuration se produit toutes les deux au moment du test. Aussi pour une configuration unique, on peut utiliser @BeforeClassdans JUnit 4.
Nils von Barth
11

Dans JUnit 4:

  • Pour la classe testée , initialisez dans une @Beforeméthode pour détecter les échecs.
  • Pour les autres classes , initialisez dans la déclaration ...
    • ... par souci de brièveté et pour marquer les champs final, exactement comme indiqué dans la question,
    • ... sauf s'il s'agit d' une initialisation complexe qui pourrait échouer, auquel cas utiliser @Beforepour intercepter les échecs.
  • Pour l'état global (en particulier une initialisation lente , comme une base de données), utilisez @BeforeClass, mais faites attention aux dépendances entre les tests.
  • L'initialisation d'un objet utilisé dans un seul test doit bien sûr être effectuée dans la méthode de test elle-même.

L'initialisation dans une @Beforeméthode ou une méthode de test vous permet d'obtenir un meilleur rapport d'erreur en cas d'échec. Ceci est particulièrement utile pour instancier la classe en cours de test (que vous pourriez casser), mais est également utile pour appeler des systèmes externes, comme l'accès au système de fichiers ("fichier non trouvé") ou la connexion à une base de données ("connexion refusée").

Il est acceptable d'avoir un standard simple et de toujours l'utiliser @Before(erreurs claires mais verbeuses) ou toujours initialiser dans la déclaration (concis mais donne des erreurs déroutantes), car les règles de codage complexes sont difficiles à suivre, et ce n'est pas un gros problème.

L'initialisation dans setUpest une relique de JUnit 3, où toutes les instances de test ont été initialisées avec empressement, ce qui pose des problèmes (vitesse, mémoire, épuisement des ressources) si vous effectuez une initialisation coûteuse. Ainsi, la meilleure pratique consistait à effectuer une initialisation coûteuse dans setUp, qui n'était exécutée que lorsque le test était exécuté. Cela ne s'applique plus, il est donc beaucoup moins nécessaire de l'utiliser setUp.

Ceci résume plusieurs autres réponses qui enterrent la lede, notamment par Craig P. Motlin (question elle-même et auto-réponse), Moss Collum (classe sous test), et dsaff.

Nils von Barth
la source
7

Dans JUnit 3, vos initialiseurs de champ seront exécutés une fois par méthode de test avant que les tests ne soient exécutés . Tant que vos valeurs de champ sont petites en mémoire, prennent peu de temps de configuration et n'affectent pas l'état global, l'utilisation des initialiseurs de champ est techniquement très bien. Cependant, si ceux-ci ne tiennent pas, vous pouvez finir par consommer beaucoup de mémoire ou de temps à configurer vos champs avant le premier test, et peut-être même manquer de mémoire. Pour cette raison, de nombreux développeurs définissent toujours des valeurs de champ dans la méthode setUp (), où elle est toujours sûre, même si ce n'est pas strictement nécessaire.

Notez que dans JUnit 4, l'initialisation des objets de test se produit juste avant l'exécution du test, et donc l'utilisation d'initialiseurs de champ est un style plus sûr et recommandé.

dsaff
la source
Intéressant. Donc, le comportement que vous avez décrit au début ne s'applique qu'à JUnit 3?
Craig P. Motlin
6

Dans votre cas (création d'une liste), il n'y a pas de différence dans la pratique. Mais en général, il est préférable d'utiliser setUp (), car cela aidera Junit à signaler correctement les exceptions. Si une exception se produit dans le constructeur / initialiseur d'un test, c'est un échec du test . Cependant, si une exception se produit lors de la configuration, il est naturel de la considérer comme un problème lors de la configuration du test, et junit le signale de manière appropriée.

amit
la source
1
bien dit. Habituez-vous simplement à toujours instancier dans setUp () et vous avez une question de moins à vous soucier - par exemple, où dois-je instancier mon fooBar, où ma collection. C'est une sorte de norme de codage à laquelle vous devez simplement adhérer. Ne vous profite pas avec des listes, mais avec d'autres instanciations.
Olaf Kock
@Olaf Merci pour l'info sur la norme de codage, je n'y avais pas pensé. J'ai tendance à être plus d'accord avec l'idée de Moss Collum d'une norme de codage.
Craig P. Motlin
5

Je préfère d'abord la lisibilité qui n'utilise le plus souvent pas la méthode de configuration. Je fais une exception lorsqu'une opération de configuration de base prend beaucoup de temps et est répétée dans chaque test.
À ce stade, je déplace cette fonctionnalité dans une méthode de configuration à l'aide de l' @BeforeClassannotation (optimisez plus tard).

Exemple d'optimisation utilisant la @BeforeClassméthode de configuration: J'utilise dbunit pour certains tests fonctionnels de base de données. La méthode de configuration est chargée de mettre la base de données dans un état connu (très lent ... 30 secondes - 2 minutes selon la quantité de données). Je charge ces données dans la méthode de configuration annotée avec @BeforeClass, puis j'exécute 10 à 20 tests sur le même ensemble de données au lieu de recharger / initialiser la base de données à l'intérieur de chaque test.

Utiliser Junit 3.8 (étendre TestCase comme indiqué dans votre exemple) nécessite d'écrire un peu plus de code que d'ajouter simplement une annotation, mais le "exécuter une fois avant la configuration de la classe" est toujours possible.

Alex B
la source
1
+1 car je préfère aussi la lisibilité. Cependant, je ne suis pas du tout convaincu que la deuxième méthode soit une optimisation.
Craig P. Motlin
@Motlin J'ai ajouté l'exemple de dbunit pour clarifier comment vous pouvez optimiser avec la configuration.
Alex B
La base de données est essentiellement un état global. Donc, déplacer la configuration de la base de données vers setUp () n'est pas une optimisation, il est nécessaire que les tests se terminent correctement.
Craig P. Motlin
@Alex B: Comme l'a dit Motlin, ce n'est pas une optimisation. Vous changez simplement où dans le code l'initialisation est effectuée, mais pas combien de fois ni à quelle vitesse.
Eddie
J'avais l'intention d'impliquer l'utilisation de l'annotation "@BeforeClass". Modification de l'exemple pour clarifier.
Alex B
2

Étant donné que chaque test est exécuté indépendamment, avec une nouvelle instance de l'objet, il n'y a pas grand intérêt à ce que l'objet Test ait un état interne à l'exception de celui partagé entre setUp()et un test individuel et tearDown(). C'est une des raisons (en plus des raisons données par d'autres) pour laquelle il est bon d'utiliser la setUp()méthode.

Remarque: C'est une mauvaise idée pour un objet de test JUnit de maintenir l'état statique! Si vous utilisez une variable statique dans vos tests pour autre chose que le suivi ou le diagnostic, vous invalidez une partie de l'objectif de JUnit, qui est que les tests peuvent (et peuvent) être exécutés dans n'importe quel ordre, chaque test s'exécutant avec un état frais et propre.

Les avantages de l'utilisation setUp()sont que vous n'avez pas à copier-coller le code d'initialisation dans chaque méthode de test et que vous n'avez pas de code de configuration de test dans le constructeur. Dans votre cas, il y a peu de différence. Créer simplement une liste vide peut être fait en toute sécurité lorsque vous l'affichez ou dans le constructeur car il s'agit d'une initialisation triviale. Cependant, comme vous et d'autres l'avez souligné, tout ce qui peut éventuellement générer un Exceptiondoit être fait setUp()afin d'obtenir le vidage de la pile de diagnostic en cas d'échec.

Dans votre cas, où vous créez simplement une liste vide, je ferais de la même manière que vous suggérez: Attribuez la nouvelle liste au point de déclaration. Surtout parce que de cette façon vous avez la possibilité de le marquerfinal si cela a du sens pour votre classe de test.

Eddie
la source
1
+1 parce que vous êtes la première personne à prendre en charge l'initialisation de la liste lors de la construction de l'objet pour la marquer comme définitive. Les choses sur les variables statiques sont cependant hors sujet pour la question.
Craig P. Motlin
@Motlin: vrai, les trucs sur les variables statiques sont un peu hors sujet. Je ne sais pas pourquoi j'ai ajouté cela, mais cela me semblait approprié à l'époque, une extension de ce que je disais au premier paragraphe.
Eddie
L'avantage de finalest cependant mentionné dans la question.
Nils von Barth
0
  • Les valeurs constantes (utilisations dans les fixtures ou les assertions) doivent être initialisées dans leurs déclarations et final(comme ne changent jamais)

  • l'objet testé doit être initialisé dans la méthode de configuration car nous pouvons activer les choses. Bien sûr, nous ne pouvons pas définir quelque chose maintenant, mais nous pourrions le définir plus tard. L'instanciation dans la méthode init faciliterait les modifications.

  • dépendances de l'objet testé si celles-ci sont moquées, ne doivent même pas être instanciées par vous-même: aujourd'hui, les frameworks fictifs peuvent l'instancier par réflexion.

Un test sans dépendance à simuler pourrait ressembler à:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Un test avec des dépendances à isoler pourrait ressembler à ceci:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
davidxxx
la source