J'essaie d'écrire des tests unitaires pour une variété d'opérations clone()
dans un grand projet et je me demande s'il existe une classe existante capable de prendre deux objets du même type, d'effectuer une comparaison approfondie et de dire si ils sont identiques ou pas?
Unitils a cette fonctionnalité:
Affirmation d'égalité par réflexion, avec différentes options telles que ignorer les valeurs Java par défaut/nul et ignorer l'ordre des collections
J'aime cette question! Principalement parce qu'il est rarement ou mal répondu. C'est comme si personne ne l'avait compris pour le moment. Territoire vierge :)
Tout d’abord, n’utilisez même pas {pensez} _ l’utilisation de equals
. Le contrat de equals
, tel que défini dans le javadoc, est une relation d'équivalence (réflexive, symétrique et transitive), not une relation d'égalité. Pour cela, il faudrait aussi qu'il soit antisymétrique. La seule implémentation de equals
qui est (ou pourrait être) une véritable relation d'égalité est celle de Java.lang.Object
. Même si vous avez utilisé equals
pour tout comparer dans le graphique, le risque de rupture du contrat est assez élevé. Comme Josh Bloch l’a souligné dans Effective Java, il est très facile de rompre le contrat d’égal à égal:
"Il n'y a simplement aucun moyen d'étendre une classe instanciable et d'ajouter un aspect tout en préservant le contrat d'égalité"
En outre, à quoi sert vraiment une méthode booléenne? Ce serait bien de résumer toutes les différences entre l'original et le clone, vous ne pensez pas? En outre, je suppose ici que vous ne voulez pas vous soucier d'écrire/maintenir le code de comparaison pour chaque objet du graphique, mais plutôt que vous cherchez quelque chose qui sera redimensionné avec la source à mesure qu'il change avec le temps.
Soooo, ce que vous voulez vraiment, c'est une sorte d'outil de comparaison d'état. La manière dont cet outil est implémenté dépend vraiment de la nature de votre modèle de domaine et de vos restrictions de performances. D'après mon expérience, il n'y a pas de solution miracle générique. Et il va être lent sur un grand nombre d'itérations. Mais pour tester l'intégralité d'une opération de clonage, cela fera très bien l'affaire. Vos deux meilleures options sont la sérialisation et la réflexion.
Quelques problèmes que vous rencontrerez:
XStream est assez rapide et combiné avec XMLUnit fera le travail en seulement quelques lignes de code. XMLUnit est agréable car il peut signaler toutes les différences, ou juste s'arrêter à la première trouvée. Et sa sortie inclut le chemin xpath vers les différents nœuds, ce qui est Nice. Par défaut, il n'autorise pas les collectes non ordonnées, mais il peut être configuré pour le faire. L'injection d'un gestionnaire de différence spécial (appelé une DifferenceListener
) vous permet de spécifier la façon dont vous souhaitez traiter les différences, y compris en ignorant l'ordre. Toutefois, dès que vous souhaitez faire autre chose que la personnalisation la plus simple, il devient difficile d’écrire et les détails tendent à être liés à un objet de domaine spécifique.
Ma préférence personnelle est d'utiliser la réflexion pour parcourir tous les champs déclarés et les approfondir, en suivant les différences au fur et à mesure. Avertissement: n'utilisez pas la récursivité à moins d'aimer les exceptions de débordement de pile. Gardez les choses à portée de la main avec une pile (utilisez une LinkedList
ou quelque chose comme ça). J'ignore généralement les champs transitoires et statiques, et j'ignore les paires d'objets que j'ai déjà comparées. Je ne me retrouve donc pas dans une boucle infinie si quelqu'un décidait d'écrire du code auto-référentiel (cependant, je compare toujours les wrappers primitifs, quoi qu'il en soit). , car les mêmes références d’objet sont souvent réutilisées). Vous pouvez configurer les éléments de manière à ignorer le classement des collections et les types ou les champs spéciaux, mais j'aime définir mes règles de comparaison d'état sur les champs eux-mêmes via des annotations. Ceci, IMHO, est exactement ce à quoi les annotations étaient destinées, pour rendre les métadonnées sur la classe disponibles au moment de l'exécution. Quelque chose comme:
@StatePolicy(unordered=true, ignore=false, exactTypesOnly=true)
private List<StringyThing> _mylist;
Je pense que c'est en fait un problème vraiment difficile, mais totalement résolable! Et une fois que vous avez quelque chose qui fonctionne pour vous, c'est vraiment très pratique :)
Alors bonne chance. Et si vous trouvez quelque chose de pur génie, n'oubliez pas de partager!
Voir DeepEquals et DeepHashCode () dans Java-util: https://github.com/jdereg/Java-util
Cette classe fait exactement ce que demande l'auteur original.
Je viens juste de mettre en œuvre la comparaison de deux instances d'entités révisées par Hibernate Envers. J'ai commencé à écrire moi-même, mais j'ai ensuite trouvé le cadre suivant.
https://github.com/SQiShER/Java-object-diff
Vous pouvez comparer deux objets du même type et il montrera les modifications, les ajouts et les suppressions. S'il n'y a pas de changement, les objets sont égaux (en théorie). Des annotations sont fournies pour les accesseurs qui doivent être ignorés lors de la vérification. Le cadre de travail a des applications beaucoup plus larges que la vérification de l’égalité, c’est-à-dire que j’utilise pour générer un journal des modifications.
Ses performances sont correctes. Lorsque vous comparez des entités JPA, veillez à les détacher d’abord du gestionnaire d’entités.
J'utilise XStream:
/**
* @see Java.lang.Object#equals(Java.lang.Object)
*/
@Override
public boolean equals(Object o) {
XStream xstream = new XStream();
String oxml = xstream.toXML(o);
String myxml = xstream.toXML(this);
return myxml.equals(oxml);
}
/**
* @see Java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
XStream xstream = new XStream();
String myxml = xstream.toXML(this);
return myxml.hashCode();
}
Vous pouvez simplement remplacer la méthode equals () de la classe à l'aide de EqualsBuilder.reflectionEquals () comme expliqué ici
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
Dans AssertJ , vous pouvez faire:
Assertions.assertThat(expectedObject).isEqualToComparingFieldByFieldRecursively(actualObject);
Cela ne fonctionnera probablement pas dans tous les cas, mais cela fonctionnera dans plus de cas que vous ne le pensez.
Voici ce que dit la documentation:
Assure que l'objet à tester (réel) est égal à la donnée objet basé sur récursif une propriété/champ par propriété/champ comparaison (y compris celles héritées). Cela peut être utile si les résultats sont égal mise en œuvre ne vous convient pas. Propriété/champ récursif la comparaison n'est pas appliquée aux champs ayant une coutume égale à l’implémentation, c’est-à-dire que la méthode égale remplacée sera utilisée d'un champ par comparaison de champ.
La comparaison récursive gère les cycles. Par défaut, les flottants sont comparé à une précision de 1.0E-6 et double avec 1.0E-15.
Vous pouvez spécifier un comparateur personnalisé par champs (imbriqués) ou taper avec respectivement usingComparatorForFields (Comparator, String ...) et usingComparatorForType (comparateur, classe).
Les objets à comparer peuvent être de types différents mais doivent avoir le mêmes propriétés/champs. Par exemple, si l'objet réel a un nom String champ, on s'attend à ce que l'autre objet en ait aussi un. Si un objet a un champ et une propriété du même nom, la valeur de la propriété sera être utilisé sur le terrain.
http://www.unitils.org/tutorial-reflectionassert.html
public class User {
private long id;
private String first;
private String last;
public User(long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
}
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);
Si vos objets implémentent Serializable, vous pouvez utiliser ceci:
public static boolean deepCompare(Object o1, Object o2) {
try {
ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
ObjectOutputStream oos1 = new ObjectOutputStream(baos1);
oos1.writeObject(o1);
oos1.close();
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
ObjectOutputStream oos2 = new ObjectOutputStream(baos2);
oos2.writeObject(o2);
oos2.close();
return Arrays.equals(baos1.toByteArray(), baos2.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Votre exemple de liste chaînée n’est pas si difficile à gérer. Lorsque le code traverse les deux graphiques d'objet, il place les objets visités dans un ensemble ou une carte. Avant de passer dans une autre référence d'objet, cet ensemble est testé pour voir si l'objet a déjà été traversé. Si c'est le cas, pas besoin d'aller plus loin.
Je suis d'accord avec la personne ci-dessus qui a dit utiliser une LinkedList (comme une pile mais sans méthodes synchronisées dessus, donc c'est plus rapide). Parcourir le graphe d'objets à l'aide d'une pile, tout en utilisant la réflexion pour obtenir chaque champ, constitue la solution idéale. Écrit une fois, cet équivalent "external" est égal à () et "externe" hashCode () est ce que toutes les méthodes equals () et hashCode () doivent appeler. Vous n’avez plus jamais besoin d’une méthode client égal à ().
J'ai écrit un peu de code qui traverse un graphe d'objet complet, répertorié dans Google Code. Voir json-io (http://code.google.com/p/json-io/). Il sérialise un graphe d'objet Java en JSON et le désérialise. Il gère tous les objets Java, avec ou sans constructeurs publics, sérialisables ou non sérialisables, etc. Ce même code de traversée sera la base de l'implémentation externe "equals ()" et externe "hashcode ()". Btw, le JsonReader/JsonWriter (json-io) est généralement plus rapide que le ObjectInputStream/ObjectOutputStream intégré.
Ce JsonReader/JsonWriter peut être utilisé à des fins de comparaison, mais cela n’aidera pas avec le hashcode. Si vous voulez un hashcode universel () et equals (), il a besoin de son propre code. Je pourrais peut-être y parvenir avec un visiteur graphique générique. Nous verrons.
Autres considérations - les champs statiques - c'est facile - ils peuvent être ignorés car toutes les instances d'equals () auraient la même valeur pour les champs statiques, car les champs statiques sont partagés entre toutes les instances.
En ce qui concerne les champs transitoires, ce sera une option sélectionnable. Parfois, vous voudrez peut-être que les passagers ne comptent pas d'autres fois. "Parfois, tu te sens comme un fou, parfois non."
Revenez sur le projet json-io (pour mes autres projets) et vous trouverez le projet externe equals ()/hashcode (). Je n'ai pas encore de nom, mais ce sera évident.
Apache vous donne quelque chose, convertit les deux objets en chaîne et les compare, mais vous devez remplacer toString ()
obj1.toString().equals(obj2.toString())
Ignorer toString ()
Si tous les champs sont des types primitifs:
import org.Apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return
ReflectionToStringBuilder.toString(this);}
Si vous avez des champs non primitifs et/ou une collection et/ou une carte:
// Within class
import org.Apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return
ReflectionToStringBuilder.toString(this,new
MultipleRecursiveToStringStyle());}
// New class extended from Apache ToStringStyle
import org.Apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.Apache.commons.lang3.builder.ToStringStyle;
import Java.util.*;
public class MultipleRecursiveToStringStyle extends ToStringStyle {
private static final int INFINITE_DEPTH = -1;
private int maxDepth;
private int depth;
public MultipleRecursiveToStringStyle() {
this(INFINITE_DEPTH);
}
public MultipleRecursiveToStringStyle(int maxDepth) {
setUseShortClassName(true);
setUseIdentityHashCode(false);
this.maxDepth = maxDepth;
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
if (value.getClass().getName().startsWith("Java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName,
Collection<?> coll) {
for(Object value: coll){
if (value.getClass().getName().startsWith("Java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
}
@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Map<?, ?> map) {
for(Map.Entry<?,?> kvEntry: map.entrySet()){
Object value = kvEntry.getKey();
if (value.getClass().getName().startsWith("Java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
value = kvEntry.getValue();
if (value.getClass().getName().startsWith("Java.lang.")
|| (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
buffer.append(value);
} else {
depth++;
buffer.append(ReflectionToStringBuilder.toString(value, this));
depth--;
}
}
}}
Hamcrest a le Matcher samePropertyValuesAs . Mais il s’appuie sur la Convention JavaBeans (utilise des accesseurs et des setters). Si les objets à comparer n'ont pas de getters et de setters pour leurs attributs, cela ne fonctionnera pas.
import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class UserTest {
@Test
public void asfd() {
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertThat(user1, samePropertyValuesAs(user2)); // all good
user2 = new User(1, "John", "Do");
assertThat(user1, samePropertyValuesAs(user2)); // will fail
}
}
Le haricot utilisateur - avec des accesseurs et des setters
public class User {
private long id;
private String first;
private String last;
public User(long id, String first, String last) {
this.id = id;
this.first = first;
this.last = last;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
}
Une garantie d’arrêt pour une comparaison aussi approfondie pourrait poser problème. Que doivent faire les personnes suivantes? (Si vous implémentez un tel comparateur, cela ferait un bon test unitaire.)
LinkedListNode a = new LinkedListNode();
a.next = a;
LinkedListNode b = new LinkedListNode();
b.next = b;
System.out.println(DeepCompare(a, b));
En voici un autre:
LinkedListNode c = new LinkedListNode();
LinkedListNode d = new LinkedListNode();
c.next = d;
d.next = c;
System.out.println(DeepCompare(c, d));
Je pense que la solution la plus simple inspirée par solution de Ray Hulha consiste à sérialiser l'objet puis à comparer en profondeur le résultat brut.
La sérialisation peut être octet, json, xml ou toString simple, etc. ToString semble être moins cher. Lombok génère ToSTring gratuit et facilement personnalisable pour nous. Voir exemple ci-dessous.
@ToString @Getter @Setter
class foo{
boolean foo1;
String foo2;
public boolean deepCompare(Object other) { //for cohesiveness
return other != null && this.toString().equals(other.toString());
}
}
Je suppose que vous le savez, mais en théorie, vous êtes censé toujours substituer .equals pour affirmer que deux objets sont vraiment égaux. Cela impliquerait qu'ils vérifient les méthodes .equals remplacées sur leurs membres.
Ce genre de chose explique pourquoi .equals est défini dans Object.
Si cela était fait systématiquement, vous n'auriez pas de problème.