Je me demandais comment faire des tests unitaires de classes abstraites et de classes qui étendent des classes abstraites.
Devrais-je tester la classe abstraite en l'étendant, en écrasant les méthodes abstraites, puis en testant toutes les méthodes concrètes? Alors seulement tester les méthodes que je substitue et tester les méthodes abstraites dans les tests unitaires pour les objets qui étendent ma classe abstraite?
Devrais-je avoir un scénario de test abstrait pouvant être utilisé pour tester les méthodes de la classe abstraite et étendre cette classe dans mon scénario de test pour les objets qui étendent la classe abstraite?
Notez que ma classe abstraite a des méthodes concrètes.
Ecrivez un objet Mock et utilisez-le uniquement pour le test. Ils sont généralement très très très minimes (hérités de la classe abstraite) et pas plus. Ensuite, dans votre test unitaire, vous pouvez appeler la méthode abstraite que vous souhaitez tester.
Vous devriez tester les classes abstraites contenant une logique comme toutes les autres classes que vous avez.
Les classes de base abstraites sont utilisées de deux manières.
Vous spécialisez votre objet abstrait, mais tous les clients utiliseront la classe dérivée via son interface de base.
Vous utilisez une classe de base abstraite pour éliminer la duplication dans les objets de votre conception, et les clients utilisent les implémentations concrètes via leurs propres interfaces.!
Solution For 1 - Strategy Pattern
Si vous avez la première situation, vous avez en fait une interface définie par les méthodes virtuelles de la classe abstraite que vos classes dérivées implémentent.
Vous devriez envisager d'en faire une interface réelle, en modifiant votre classe abstraite pour qu'elle soit concrète, et en prenant une instance de cette interface dans son constructeur. Vos classes dérivées deviennent alors des implémentations de cette nouvelle interface.
Cela signifie que vous pouvez maintenant tester votre classe précédemment abstraite à l'aide d'une instance fictive de la nouvelle interface et de chaque nouvelle implémentation via l'interface désormais publique. Tout est simple et testable.
Solution For 2
Si vous avez la deuxième situation, votre classe abstraite fonctionne comme une classe d'assistance.
Jetez un coup d'œil à la fonctionnalité qu'il contient. Voyez si une partie de celle-ci peut être poussée sur les objets en cours de manipulation pour minimiser cette duplication. S'il vous reste encore quelque chose, envisagez de faire de cette classe une classe d'assistance que votre implémentation concrète prenne dans son constructeur et supprime sa classe de base.
Cela conduit à nouveau à des classes concrètes simples et facilement testables.
En règle
Privilégiez les réseaux complexes d'objets simples sur un réseau simple d'objets complexes.
La clé du code testable extensible est constituée de petits blocs de construction et d’un câblage indépendant.
Mise à jour: Comment gérer les mélanges des deux?
Il est possible qu'une classe de base exécute ces deux rôles ... c'est-à-dire qu'elle possède une interface publique et des méthodes d'assistance protégées. Si tel est le cas, vous pouvez alors factoriser les méthodes d'assistance dans une classe (scenario2) et convertir l'arborescence d'héritage en un modèle de stratégie.
Si vous constatez que certaines méthodes sont directement implémentées par votre classe de base et que d'autres sont virtuelles, vous pouvez toujours convertir l'arborescence d'héritage en un modèle de stratégie, mais je le prendrais également comme un bon indicateur du fait que les responsabilités ne sont pas correctement alignées. besoin de refactoring.
Mise à jour 2: Les classes abstraites comme point de départ (12/06/2014)
L’autre jour, j’ai utilisé l’abrégé dans une situation où j’aimerais explorer pourquoi.
Nous avons un format standard pour nos fichiers de configuration. Cet outil particulier a 3 fichiers de configuration, tous dans ce format. Je voulais une classe fortement typée pour chaque fichier de paramètres. Ainsi, grâce à l'injection de dépendances, une classe pouvait demander les paramètres dont elle se souciait.
J'ai implémenté cela en disposant d'une classe de base abstraite qui sait comment analyser les formats de fichiers de paramètres et les classes dérivées qui exposaient ces mêmes méthodes, mais qui encapsulait l'emplacement du fichier de paramètres.
J'aurais pu écrire un "SettingsFileParser" encapsulé par les 3 classes, puis délégué à la classe de base pour exposer les méthodes d'accès aux données. J'ai choisi de ne pas encore le faire car cela conduirait à 3 classes dérivées avec plus de délégations code en eux que toute autre chose.
Cependant ... à mesure que ce code évolue et que les consommateurs de chacune de ces classes de paramètres deviennent plus clairs. Chaque paramètre demande aux utilisateurs de définir certains paramètres et de les transformer (les paramètres étant du texte, ils peuvent les envelopper dans des objets, les convertir en chiffres, etc.). Lorsque cela se produit, je vais commencer à extraire cette logique dans des méthodes de manipulation de données et les repousser dans les classes de paramètres fortement typés. Cela mènera à une interface de niveau supérieur pour chaque ensemble de paramètres, c'est-à-dire qu'elle n'est plus consciente qu'il s'agit de "paramètres".
À ce stade, les classes de paramètres fortement typées n'auront plus besoin des méthodes "getter" qui exposent l'implémentation de "paramètres" sous-jacente.
À ce stade, je ne voudrais plus que leur interface publique inclue les méthodes d'accès des paramètres; donc je vais changer cette classe pour encapsuler une classe d'analyseur de paramètres au lieu d'en dériver.
La classe abstraite est donc: un moyen pour moi d'éviter le code de délégation pour le moment, et un marqueur dans le code pour me rappeler de modifier le dessin ultérieurement. Il se peut que je n’y arrive jamais, alors cela peut vivre un bon moment ... seul le code peut le dire.
Je trouve que cela est vrai pour n'importe quelle règle ... comme "pas de méthodes statiques" ou "pas de méthodes privées". Ils indiquent une odeur dans le code ... et c'est bien. Il vous permet de rester à la recherche de l'abstraction que vous avez manquée… et de continuer à offrir de la valeur à vos clients en attendant.
J'imagine des règles comme celle-ci définissant un paysage, où un code maintenable habite dans les vallées. Lorsque vous ajoutez un nouveau comportement, c'est comme si de la pluie se posait sur votre code. Au début, vous le mettez partout où il atterrit .. puis vous refactorisez pour permettre aux forces de bonne conception de pousser le comportement jusqu'à ce que tout se retrouve dans les vallées.
Ce que je fais pour les classes abstraites et les interfaces est le suivant: J'écris un test, qui utilise l'objet comme il est concret. Mais la variable de type X (X est la classe abstraite) n'est pas définie dans le test. Cette classe de test n'est pas ajoutée à la suite de tests, mais à ses sous-classes, qui ont une méthode d'installation qui définit la variable sur une implémentation concrète de X. De cette manière, je ne duplique pas le code de test. Les sous-classes du test non utilisé peuvent ajouter d'autres méthodes de test si nécessaire.
Si votre classe abstraite contient des fonctionnalités concrètes ayant une valeur pour l'entreprise, je la teste généralement directement en créant un test double qui remplace les données abstraites ou en utilisant un cadre moqueur pour le faire à ma place. Le choix que je choisis dépend beaucoup de la nécessité d'écrire ou non des implémentations spécifiques aux tests des méthodes abstraites.
Le scénario le plus courant dans lequel je dois effectuer cette opération est lorsque j'utilise le modèle de modèle , comme lorsque je construis une sorte de cadre extensible qui sera utilisé par une tierce partie. Dans ce cas, c'est la classe abstraite qui définit l'algorithme que je veux tester. Il est donc plus logique de tester la base abstraite qu'une implémentation spécifique.
Cependant, je pense qu'il est important que ces tests se concentrent sur les implémentations concrètes de la vraie logique métier uniquement ; vous ne devriez pas tester unitairement les détails d'implémentation de la classe abstraite, car vous allez vous retrouver avec des tests fragiles.
Pour créer un test unitaire spécifiquement sur la classe abstraite, vous devez le dériver à des fins de test, tester les résultats de base.method () et le comportement souhaité lors de l'héritage.
Vous testez une méthode en l'appelant, alors testez une classe abstraite en l'implémentant ...
une solution consiste à écrire un scénario de test abstrait qui correspond à votre classe abstraite, puis à écrire des scénarios de test concrets qui sous-classent votre scénario de test abstrait. faites cela pour chaque sous-classe concrète de votre classe abstraite d'origine (c'est-à-dire que votre hiérarchie de cas de test reflète celle de votre classe). voir Tester une interface dans le livre de recettes Junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .
voir aussi Testcase Superclass dans les modèles xUnit: http://xunitpatterns.com/Testcase%20Superclass.html
Je discuterais contre les tests "abstraits". Je pense qu'un test est une idée concrète et n'a pas d'abstraction. Si vous avez des éléments communs, mettez-les dans des méthodes auxiliaires ou des classes utilisables par tous.
Pour ce qui est de tester une classe de tests abstraits, assurez-vous de vous demander ce que vous testez. Il existe plusieurs approches et vous devez savoir ce qui fonctionne dans votre scénario. Essayez-vous de tester une nouvelle méthode dans votre sous-classe? Ensuite, vos tests n'interagissent qu'avec cette méthode. Testez-vous les méthodes dans votre classe de base? Ensuite, possédez probablement un appareil séparé uniquement pour cette classe et testez chaque méthode individuellement avec autant de tests que nécessaire.
Voici le schéma que je suis généralement en train de suivre lors de la configuration d'un harnais pour tester une classe abstraite:
public abstract class MyBase{
/*...*/
public abstract void VoidMethod(object param1);
public abstract object MethodWithReturn(object param1);
/*,,,*/
}
Et la version que j'utilise sous test:
public class MyBaseHarness : MyBase{
/*...*/
public Action<object> VoidMethodFunction;
public override void VoidMethod(object param1){
VoidMethodFunction(param1);
}
public Func<object, object> MethodWithReturnFunction;
public override object MethodWithReturn(object param1){
return MethodWihtReturnFunction(param1);
}
/*,,,*/
}
Si les méthodes abstraites sont appelées alors que je ne m'y attendais pas, les tests échouent. Lors de l'organisation des tests, je peux facilement remplacer les méthodes abstraites avec lambdas qui effectuent des assertions, lèvent des exceptions, renvoient des valeurs différentes, etc.
Si les méthodes concrètes invoquent l'une des méthodes abstraites, cette stratégie ne fonctionnera pas et vous voudrez tester chaque comportement de classe enfant séparément. Sinon, il serait bon de l'étendre et de modifier les méthodes abstraites décrites précédemment, à condition que les méthodes concrètes de la classe abstraite soient découplées des classes enfants.
L'une des principales motivations de l'utilisation d'une classe abstraite est d'activer le polymorphisme au sein de votre application, c'est-à-dire: vous pouvez remplacer une version différente au moment de l'exécution. En fait, c’est à peu près la même chose que d’utiliser une interface, sauf que la classe abstraite fournit des éléments communs de plomberie, souvent appelés Modèle de modèle.
Du point de vue des tests unitaires, il y a deux choses à considérer:
Interaction de votre classe abstraite avec les classes associées. L'utilisation d'un framework de tests factices est idéale pour ce scénario car elle montre que votre classe abstraite fonctionne bien avec les autres.
Fonctionnalité des classes dérivées. Si vous avez écrit une logique personnalisée pour vos classes dérivées, vous devez tester ces classes séparément.
edit: RhinoMocks est un framework de tests fictifs impressionnant qui peut générer des objets fictifs au moment de l’exécution en dérivant dynamiquement de votre classe. Cette approche peut vous faire économiser d'innombrables heures de classes dérivées de codage manuel.
Premièrement, si la classe abstraite contenait une méthode concrète, je pense que vous devriez le faire en tenant compte de cet exemple.
public abstract class A
{
public boolean method 1
{
// concrete method which we have to test.
}
}
class B extends class A
{
@override
public boolean method 1
{
// override same method as above.
}
}
class Test_A
{
private static B b; // reference object of the class B
@Before
public void init()
{
b = new B ();
}
@Test
public void Test_method 1
{
b.method 1; // use some assertion statements.
}
}
Je suppose que vous voudrez peut-être tester les fonctionnalités de base d'une classe abstraite ... Mais vous seriez probablement mieux en étendant la classe sans surcharger les méthodes et en faisant un effort minimal pour se moquer des méthodes abstraites.
Après la réponse de @ patrick-desjardins, j’ai mis en œuvre abstract et sa classe d’implémentation avec @Test
comme suit:
Classe abstraite - ABC.Java
import Java.util.ArrayList;
import Java.util.List;
public abstract class ABC {
abstract String sayHello();
public List<String> getList() {
final List<String> defaultList = new ArrayList<>();
defaultList.add("abstract class");
return defaultList;
}
}
As Les classes abstraites ne peuvent pas être instanciées, mais elles peuvent être sous-classées , classe concrète DEF.Java , est comme suit:
public class DEF extends ABC {
@Override
public String sayHello() {
return "Hello!";
}
}
@ Test classe pour tester les méthodes abstraites et non abstraites:
import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import Java.util.Collection;
import Java.util.List;
import static org.hamcrest.Matchers.equalTo;
import org.junit.Test;
public class DEFTest {
private DEF def;
@Before
public void setup() {
def = new DEF();
}
@Test
public void add(){
String result = def.sayHello();
assertThat(result, is(equalTo("Hello!")));
}
@Test
public void getList(){
List<String> result = def.getList();
assertThat((Collection<String>) result, is(not(empty())));
assertThat(result, contains("abstract class"));
}
}
Si une classe abstraite convient à votre implémentation, testez (comme suggéré ci-dessus) une classe concrète dérivée. Vos hypothèses sont correctes.
Pour éviter toute confusion future, sachez que cette classe de test concrète n’est pas une simulation, mais un fake.
Strictement, un mock est défini par les caractéristiques suivantes: