junit 如何编写单元测试来检查复制构造函数是否与类属性同步?

sxpgvts3  于 12个月前  发布在  其他
关注(0)|答案(3)|浏览(173)

最近,我们的系统中出现了一个错误,这是由于忘记在复制构造函数中分配一个新添加的类属性造成的。
举例来说:

public class MyClass {

    private Long companyId;
    private Long classId;
    private Double value;   <=  newly added

    public MyClass(MyClass myClass) {
        this.setCompanyId(myClass.getCompanyId());
        this.setClassId(myClass.getClassId());
        this.setValue(myClass.getValue());   <= we forget this line
    }

字符串
我想写一个单元测试,保证在添加新属性时捕获丢失的复制赋值。我应该如何进行?

nhaq1z21

nhaq1z211#

IMO,有三种方法可以解决这个问题
1.使用自动更新的复制构造函数(例如使用Lombok)

@Builder(toBuilder=true)
class Foo {
   int x;
   ...
}
Foo f0 = Foo.builder().build();
Foo f1 = f0.toBuilder().build();  // copy

字符串
1.创建一个用非默认值填充对象的测试,然后使用反射/Jackson读取其所有键值对,并确保该值不是默认值(null00.0false'\0'

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
// other imports

public class FooTest {
  @Test
  public void testCopyConstructor_shouldCopyAllFields() {
    ObjectMapper mapper = new ObjectMapper();
    // only use field, don't use getters
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
    mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

    // should be instantiated with non-default values
    Foo original = new Foo();
    original.setSimilarity(0.1);
    original.setStatus(SomeEnum.PENDING);
    // ...

    Foo copied = new Foo(original);

    Map<String, Object> originalMap = mapper.convertValue(original, new TypeReference<HashMap<String, Object>>(){});
    for (Map.Entry<String, Object> entry : originalMap.entrySet()) {
      // if we add new fields and forget to update this test, this test should fail
      assertTrue(isNotDefaultValue(entry.getValue()), "Forgot to update this test: " + entry.getKey());
    }

    Map<String, Object> resultingMap = mapper.convertValue(copied, new TypeReference<HashMap<String, Object>>(){});
    for (Map.Entry<String, Object> entry : resultingMap.entrySet()) {
      Object expectedValue = originalMap.get(entry.getKey());
      assertEquals(entry.getValue(), expectedValue);
    }
  }

  private static boolean isNotDefaultValue(Object value){
    if (value == null)
      return false;
    boolean defaultBoolean = false;
    char defaultChar = '\0';
    byte defaultByte = 0;
    short defaultShort = 0;
    int defaultInt = 0;
    long defaultLong = 0;
    float defaultFloat = 0;
    double defaultDouble = 0;
    List<Object> defaultValues = List.of(defaultBoolean, defaultChar, defaultByte, defaultShort,
      defaultInt, defaultLong, defaultFloat, defaultDouble);
    return !defaultValues.contains(value);
  }
}


1.使用一个复杂的对象随机数发生器,比如EasyRandom,并在单元测试中使用它,并执行复制构造函数

import java.util.Objects;
// other imports

public class FooTest {
  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

  @Test
  public void testCopyConstructor_shouldCopyAllFields() {
    EasyRandom generator = new EasyRandom();
    
    for (int i = 0; i < 4; i++){  // you can do it multiple times if you're unsure
      Person person = generator.nextObject(Person.class);
      Person copied = new Person(person);

      // or if you afraid of forgetting to update your .equals() method, 
      // just convert both of them to hashmaps (using jackson) 
      // and compare the hashmaps
      assertEquals(copied, person);  
    }
  }
}

uhry853o

uhry853o2#

我会用反射来做这个。

  • 新建一个MyClass示例。(A)
  • 通过反射,为 every 字段赋值。这可以通过getClass().getDeclaredFields()完成。
  • 新建一个MyClass的空示例。(B)
  • 对A对B运行复制构造函数。
  • 通过反射,确定A中的字段是否等于B。
ercv8c1e

ercv8c1e3#

**1)**在Makoto的帖子中,我曾经使用过一种方法来通过编程控制克隆的正确性。但这是一件很难和很长的事情。

我给予你我用来做单元测试的代码。它肯定是可以完善的:

/**
 * Vérification du bon clonage d'un objet.
 * @param o Objet original.
 * @param clone Objet qui se dit clone du premier.
 * @param champsExclus Champs qu'il faut exclure de la vérification d'assertNotSame().
 */
public void controlerClonage(Object o, Object clone, String... champsExclus) {
   assertNotNull(o, MessageFormat.format("L''objet de classe {0} dont le clonage doit être contrôlé ne peut pas valoir null.", o.getClass().getName()));
   assertNotNull(clone, MessageFormat.format("L''objet de classe {0} dont le clonage doit être contrôlé ne peut pas valoir null.", o.getClass().getName()));

   // Cloner l'objet puis vérifier qu'il est égal à son original.
   assertEquals(o, clone, MessageFormat.format("L''objet cloné {0} et son original devraient être égaux.", o.getClass().getSimpleName()));
   assertNotSame(o, clone, MessageFormat.format("L''objet cloné {0} et son original ne devraient pas partager le même pointeur.", o.getClass().getSimpleName()));

   // Enumérer toutes les variables membres de l'objet.
   Field[] champs = o.getClass().getDeclaredFields();

   for(Field champ : champs) {
      // Si le champ est parmi la liste de ceux à exclure, ne pas en tenir compte.
      if (isChampExclu(champ, champsExclus)) {
         continue;
      }

      // Si l'objet est un type primitif ou un String, l'assertNotSame() ne pourra pas s'appliquer.
      // En revanche, l'assertEquals(), lui, toujours.
      champ.setAccessible(true);

      // Lecture de la valeur originale.
      Object valeurOriginal = null;

      try {
         valeurOriginal = champ.get(o);
      }
      catch(IllegalArgumentException | IllegalAccessException e) {
         fail(MessageFormat.format("L''obtention de la valeur du champ {0} dans l''objet original {1} a échoué : {2}.", champ.getName(),
               o.getClass().getSimpleName(), e.getMessage()));
      }

      // Lecture de la valeur clonée.
      Object valeurClone = null;

      try {
         valeurClone = champ.get(clone);
      }
      catch(IllegalArgumentException | IllegalAccessException e) {
         fail(MessageFormat.format("L''obtention de la valeur du champ {0} dans l''objet cloné {1} a échoué : {2}.", champ.getName(),
               clone.getClass().getSimpleName(), e.getMessage()));
      }

      assertEquals(valeurOriginal, valeurClone, MessageFormat.format("La valeur de la variable membre {0} de l''objet {1} et de son clone devrait être égales.", champ.getName(), clone.getClass().getSimpleName()));

      // Les types primitifs, les chaînes de caractères et les énumérations, ne voient pas leurs pointeurs vérifiés.
      // Et cela n'a de sens que si les valeurs de ces pointeurs sont non nuls.
      if (valeurOriginal != null && valeurClone != null) {
         if (champ.getType().isPrimitive() == false && champ.getType().equals(String.class) == false
               && Enum.class.isAssignableFrom(champ.getType()) == false)
            assertNotSame(valeurOriginal, valeurClone, MessageFormat.format("La variable membre {0} de l''objet {1} et de son clone ne devraient pas partager le même pointeur.", champ.getName(), clone.getClass().getSimpleName()));
      }
   }
}

/**
 * Déterminer si un champ fait partie d'une liste de champs exclus.
 * @param champ Champ.
 * @param champsExclus Liste de champs exclus.
 * @return true, si c'est le cas.
 */
private boolean isChampExclu(Field champ, String[] champsExclus) {
   for (String exclus : champsExclus) {
      if (exclus.equals(champ.getName()))
         return true;
   }

   return false;
}

/**
 * Alimenter un objet avec des valeurs par défaut.
 * @param objet Objet.
 * @return Objet lui-même (alimenté).
 */
public Object alimenter(Object objet) {
   double nDouble = 1.57818;
   int nInt = 1;
   long nLong = 10000L;
   
   Class<?> classe = objet.getClass();
   
   while(classe.equals(Object.class) == false && classe.getName().startsWith("java.") == false) {
      for(Field field : classe.getDeclaredFields()) {
         // Ignorer certains types
         if (field.getType().equals(Class.class) || field.getType().equals(ArrayList.class)
            || field.getType().equals(List.class)|| field.getType().equals(Set.class)
            || field.getType().equals(HashSet.class) || field.getType().equals(HashMap.class)) {
            continue;
         }
         
         // Ecarter les champs statiques.
         if (Modifier.isStatic(field.getModifiers())) {
            continue;
         }
         
         // Champs de type texte.
         if (field.getType().equals(String.class)) {
            setValue(field, objet, "*" + field.getName() + "*");
            continue;
         }
         
         // Champs de type LocalDate.
         if (field.getType().equals(LocalDate.class)) {
            setValue(field, objet, LocalDate.now());
            continue;
         }
         
         // Champs de type LocalDateTime.
         if (field.getType().equals(LocalDateTime.class)) {
            setValue(field, objet, LocalDateTime.now());
            continue;
         }
         
         // Champs de type URL.
         if (field.getType().equals(URL.class)) {
            try {
               URL url = new URL("http://a.b.c/test");
               setValue(field, objet, url);
            }
            catch(MalformedURLException e) {
               throw new RuntimeException("Mauvaise préparation d'URL pour test toString : " + e.getMessage());
            }
            
            continue;
         }
         
         // Champs de type ZonedDateTime.
         if (field.getType().equals(ZonedDateTime.class)) {
            setValue(field, objet, ZonedDateTime.now());
            continue;
         }
         
         // Champs de type texte.
         if (field.getType().equals(Double.class) || field.getType().equals(Double.TYPE)) {
            setValue(field, objet, nDouble ++);
            continue;
         }
         
         // Champs de type integer.
         if (field.getType().equals(Integer.class) || field.getType().equals(Integer.TYPE)) {
            setValue(field, objet, nInt ++);
            continue;
         }
         
         // Champs de type long.
         if (field.getType().equals(Long.class) || field.getType().equals(Long.TYPE)) {
            setValue(field, objet, nLong ++);
            continue;
         }
         
         // Champs de type énumération
         if (Enum.class.isAssignableFrom(field.getType())) {
            @SuppressWarnings("unchecked") 
            Class<Enum<?>> enumeration = (Class<Enum<?>>)field.getType();
            Enum<?>[] constantes = enumeration.getEnumConstants();
            setValue(field, objet, constantes[0]);
            
            // On en profite pour vérifier toutes ses constantes.
            for(Enum<?> constante : constantes) {
               System.out.println(MessageFormat.format("{0}.{1}.{2} = {3}", classe.getName(), field.getName(), constante.name(), constante.toString()));
            }
            
            continue;
         }
         
         if (field.getType().getName().startsWith("java.") == false) {
            try {
               field.setAccessible(true);
               Object membre = field.get(objet);
               
               // Ecarter les champs statiques et abstraits.
               if ((Modifier.isStatic(field.getModifiers()) && Modifier.isAbstract(field.getModifiers())) == false) {
                  // Si l'objet n'est pas initialisé, tenter de le faire.
                  if (membre == null) {
                     try {
                        membre = field.getType().getDeclaredConstructor().newInstance();
                     }
                     catch(@SuppressWarnings("unused") InstantiationException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
                        // On laisse passer, on ne pourra  pas l'attribuer.
                     }
                  }
                  
                  // Si l'on a obtenu le membre ou si on l'a créé, on l'alimente et on l'affecte.
                  if (membre != null && membre != objet) {
                     alimenter(membre);
                     setValue(field, objet, membre);
                  }
               }
               
               continue;
            }
            catch(IllegalArgumentException | IllegalAccessException e) {
               System.err.println(MessageFormat.format("{0}.{1}.{2} n'a pas pu être assigné : {3}", objet.getClass().getName(), field.getName(), e.getMessage()));
            }
         }

         // Indiquer les champs que l'on a pas pu assigner.
         System.err.println(MessageFormat.format("non assigné : {0}.{1}", field.getName(), field.getType().getName()));
      }
      
      classe = classe.getSuperclass();
   }
   
   return objet;
}

/**
 * Fixer une valeur à un champ.
 * @param field Champ dans l'objet cible.
 * @param objet Objet cible.
 * @param valeur Valeur à attribuer.
 */
private void setValue(Field field, Object objet, Object valeur) {
   field.setAccessible(true);
   
   try {
      field.set(objet, valeur);
   }
   catch(IllegalArgumentException | IllegalAccessException e) {
      System.err.println(MessageFormat.format("{0}.{1} n'a pas pu être assigné : {2}", objet.getClass().getName(), field.getName(), e.getMessage()));
   }
}

字符串

**2)**这不是关于U.T.的自动控制,而是关于IDE内部的开发时间:

这些天来,我删除了所有的clone()方法,因为我知道它们是一个不好的做法,用复制构造函数来代替它们。
我注意到,如果像 IntelliJ 这样的IDE在类MyClass中检测到MyClass(MyClass source) {...}形式的构造函数,如果缺少一个赋值,它就会发出警告。


的数据
类名称用黄色包围,并带有消息:Copy constructor does not copy field 'population'

相关问题