Comment les tests unitaires doivent-ils configurer les sources de données lorsqu'ils ne s'exécutent pas sur un serveur d'applications?

StackOverflow https://stackoverflow.com/questions/831760

Question

Merci à tous pour votre aide. Un certain nombre d'entre vous ont posté (comme j'aurais dû m'y attendre) des réponses indiquant que mon approche était mauvaise ou que le code de bas niveau ne devrait jamais avoir à savoir s'il est exécuté ou non dans un conteneur. J'aurais tendance à être d'accord. Toutefois, j’ai affaire à une ancienne application complexe et n’ai pas la possibilité de procéder à une refactorisation majeure du problème actuel.

Permettez-moi de revenir en arrière et de poser la question motivée à ma question initiale.

J'ai une application héritée sous JBoss et j'ai apporté quelques modifications au code de niveau inférieur. J'ai créé un test unitaire pour ma modification. Pour exécuter le test, je dois me connecter à une base de données.

Le code hérité récupère la source de données de la manière suivante:

(jndiName est une chaîne définie)

Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);

Mon problème est que, lorsque j'exécute ce code sous test unitaire, aucune source de données n'est définie dans le contexte. Ma solution à cela était d'essayer de voir si j'exécutais sous le serveur d'applications et, sinon, de créer le test DataSource et de le renvoyer. Si je suis sous le serveur d'applications, j'utilise le code ci-dessus.

Ma question réelle est donc la suivante: quelle est la bonne façon de procéder? Existe-t-il une manière approuvée par le test unitaire de configurer le contexte pour renvoyer la source de données appropriée de sorte que le code testé ne nécessite pas de savoir où il s'exécute?

Pour le contexte: MA QUESTION ORIGINALE:

J'ai du code Java qui doit savoir s'il fonctionne ou non sous JBoss. Existe-t-il un moyen canonique pour le code d’indiquer s’il fonctionne dans un conteneur?

Ma première approche a été développée expérimentalement et consiste à obtenir le contexte initial et à vérifier qu’elle peut rechercher certaines valeurs.

private boolean isRunningUnderJBoss(Context ctx) {
        boolean runningUnderJBoss = false;
        try {
            // The following invokes a naming exception when not running under
            // JBoss.
            ctx.getNameInNamespace();

            // The URL packages must contain the string "jboss".
            String urlPackages = (String) ctx.lookup("java.naming.factory.url.pkgs");
            if ((urlPackages != null) && (urlPackages.toUpperCase().contains("JBOSS"))) {
                runningUnderJBoss = true;
            }
        } catch (Exception e) {
            // If we get there, we are not under JBoss
            runningUnderJBoss = false;
        }
        return runningUnderJBoss;
    }

Context ctx = new InitialContext();
if (isRunningUnderJboss(ctx)
{
.........

Maintenant, cela semble fonctionner, mais cela ressemble à un bidouillage. Quel est le " correct " façon de faire ça? Idéalement, j'aimerais une méthode qui fonctionnerait avec une variété de serveurs d'applications, pas seulement JBoss.

Était-ce utile?

La solution

Toute l’approche semble ne pas aller vers moi. Si votre application a besoin de savoir dans quel conteneur elle s'exécute, vous faites quelque chose de mal.

Lorsque j'utilise Spring, je peux passer de Tomcat à WebLogic et inversement sans rien changer. Je suis sûr qu'avec une configuration adéquate, je pourrais faire la même chose avec JBOSS. C’est l’objectif pour lequel je visais.

Autres conseils

Tout le concept est de retour. Le code de niveau inférieur ne devrait pas faire ce genre de test. Si vous avez besoin d’une autre implémentation, transmettez-la à un point pertinent.

Une combinaison d'injection de dépendance (via Spring, fichiers de configuration ou arguments de programme) et le modèle d'usine fonctionnerait généralement mieux.

À titre d'exemple, je passe un argument à mes scripts Ant qui configurent des fichiers de configuration, selon que l'oreille ou la guerre est implémentée dans un environnement de développement, de test ou de production.

Peut-être quelque chose comme ça (moche mais ça peut marcher)

 private void isRunningOn( String thatServerName ) { 

     String uniqueClassName = getSpecialClassNameFor( thatServerName );
     try { 
         Class.forName( uniqueClassName );
     } catch ( ClassNotFoudException cnfe ) { 
         return false;
     }
     return true;
 } 

La méthode getSpecialClassNameFor renvoie une classe unique pour chaque serveur d'applications (et peut renvoyer de nouveaux noms de classe lorsque plusieurs serveurs d'applications sont ajoutés)

Ensuite, vous l'utilisez comme:

  if( isRunningOn("JBoss")) {
         createJBossStrategy....etcetc
  }
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);

Qui construit le InitialContext? Sa construction doit être en dehors du code que vous essayez de tester, sinon vous ne pourrez pas vous moquer du contexte.

Puisque vous avez dit que vous travailliez sur une application héritée, commencez par refactoriser le code afin que vous puissiez facilement injecter le contexte ou la source de données dans la classe. Ensuite, vous pourrez plus facilement écrire des tests pour cette classe.

Vous pouvez effectuer la transition du code hérité en ayant deux constructeurs, comme dans le code ci-dessous, jusqu'à ce que vous ayez restructuré le code qui construit la classe. De cette façon, vous pouvez plus facilement tester Foo et garder le code qui utilise Foo inchangé. Ensuite, vous pouvez lentement refactoriser le code, de sorte que l'ancien constructeur soit complètement supprimé et que toutes les dépendances soient des dépendances injectées.

public class Foo {
  private final DataSource dataSource;
  public Foo() { // production code calls this - no changes needed to callers
    Context ctx = new InitialContext();
    this.dataSource = (DataSource) ctx.lookup(jndiName);
  }
  public Foo(DataSource dataSource) { // test code calls this
    this.dataSource = dataSource;
  }
  // methods that use dataSource
}

Mais avant de commencer cette refactorisation, vous devez passer des tests d’intégration pour couvrir votre dos. Sinon, vous ne pouvez pas savoir si même les simples refactorisations, telles que le déplacement de la recherche DataSource vers le constructeur, cassent quelque chose. Ensuite, lorsque le code sera meilleur, plus testable, vous pourrez écrire des tests unitaires. (Par définition, si un test touche le système de fichiers, le réseau ou la base de données, ce n'est pas un test unitaire, c'est un test d'intégration.)

L'avantage des tests unitaires est qu'ils s'exécutent rapidement (des centaines ou des milliers par seconde) et qu'ils sont très concentrés sur le test d'un comportement à la fois. Cela permet de l'exécuter souvent (si vous hésitez à exécuter tous les tests unitaires après avoir modifié une ligne, ils s'exécutent trop lentement) afin que vous obteniez un retour rapide. Et comme ils sont très concentrés, il suffit de regarder le nom du test ayant échoué pour savoir exactement où se trouve le bogue dans le code de production.

L'avantage des tests d'intégration est qu'ils permettent de s'assurer que toutes les pièces sont correctement connectées. C’est également important, mais vous ne pouvez pas les exécuter très souvent car des opérations telles que toucher la base de données les ralentissent. Cependant, vous devez toujours les exécuter au moins une fois par jour sur votre serveur d'intégration continue.

Il existe plusieurs façons de résoudre ce problème. L'une consiste à transmettre un objet Context à la classe lorsqu'elle est sous test unitaire. Si vous ne pouvez pas modifier la signature de la méthode, refactorisez la création du contexte initial en une méthode protégée et testez une sous-classe qui renvoie l'objet de contexte simulé en remplaçant la méthode. Cela peut au moins mettre la classe à l’épreuve de sorte que vous puissiez réfléchir à de meilleures alternatives à partir de là.

L'option suivante consiste à créer une fabrique de connexions de base de données pouvant indiquer s'il se trouve ou non dans un conteneur, et à prendre les mesures appropriées dans chaque cas.

Une chose à laquelle il faut penser est qu'une fois la connexion à la base de données libérée du conteneur, qu'allez-vous faire? C’est plus facile, mais ce n’est pas tout à fait un test unitaire si vous devez transporter l’ensemble de la couche d’accès aux données.

Pour vous aider davantage dans la progression du code hérité sous test unitaire, je vous suggère de consulter le Travailler efficacement avec le code existant .

Une façon simple de procéder consiste à configurer les écouteurs de cycle de vie dans web.xml . Ceux-ci peuvent définir des indicateurs globaux si vous le souhaitez. Par exemple, vous pouvez définir un ServletContextListener . Dans votre web.xml et dans la méthode contextInitialized , définissez un indicateur global que vous exécutez dans un conteneur. Si l'indicateur global n'est pas défini, vous ne travaillez pas dans un conteneur.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top