c# - mvc - Pruebas unitarias para la clonación profunda
pruebas unitarias c# (6)
Digamos que tengo una clase .NET compleja, con muchas matrices y otros miembros de objetos de clase. Necesito poder generar un clon profundo de este objeto, así que escribo un método Clone (), y lo implemento con un sencillo BinaryFormatter serialize / deserialize - o tal vez hago el clon profundo usando alguna otra técnica que es más propensa a errores y me gustaría asegurarme de que sea probado
OK, entonces ahora (vale, debería haberlo hecho primero) Me gustaría escribir pruebas que cubran la clonación. Todos los miembros de la clase son privados, y mi arquitectura es tan buena (!) Que no he necesitado escribir cientos de propiedades públicas u otros accesorios. La clase no es IComparable o IEquatable, porque la aplicación no la necesita. Las pruebas de mi unidad están en un ensamblaje separado del código de producción.
¿Qué enfoques toman las personas para probar que el objeto clonado es una buena copia? ¿Escribes (o reescribes una vez que descubres la necesidad del clon) todas las pruebas de tu unidad para la clase para que puedan invocarse con un objeto "virgen" o con un clon de él? ¿Cómo probarías si parte de la clonación no fuera lo suficientemente profunda, ya que este es solo el tipo de problema que puede dar lugar a errores horribles para encontrar luego?
Solo escribiría una prueba para determinar si el clon era correcto o no. Si la clase no está sellada, puede crear un arnés para ella extendiéndola y luego exponiendo todas sus partes internas dentro de la clase infantil. Alternativamente, puede usar reflection (yech) o usar los generadores de Accessor de MSTest.
Necesita clonar su objeto y luego revisar cada propiedad y variable que tenga su objeto y determinar si se copió correctamente o se clonó correctamente.
Su método de prueba dependerá del tipo de solución que se le ocurra. Si escribe algún código de clonación personalizado y tiene que implementarlo manualmente en cada tipo de clonación, entonces realmente debería probar la clonación de cada uno de esos tipos. Alternativamente, si decide ir por una ruta más genérica (donde probablemente encajaría la reflexión antes mencionada), sus pruebas solo necesitarían probar los escenarios específicos con los que su sistema de clonación tendrá que lidiar.
Para responder a sus preguntas específicas:
¿Escribes (o reescribes una vez que descubres la necesidad del clon) todas las pruebas de tu unidad para la clase para que puedan invocarse con un objeto "virgen" o con un clon de él?
Debería tener pruebas para todos los métodos que se pueden realizar tanto en el objeto original como en el clonado. Tenga en cuenta que debe ser muy fácil configurar un diseño de prueba simple para admitir esto sin actualizar manualmente la lógica de cada prueba.
¿Cómo probarías si parte de la clonación no fuera lo suficientemente profunda, ya que este es solo el tipo de problema que puede dar lugar a errores horribles para encontrar luego?
Depende del método de clonación que elija. Si tiene que actualizar manualmente los tipos clonables, debe probar que cada tipo clone todos (y solo) los miembros que espera. Mientras que, si está probando un marco de clonación, crearía algunos tipos clonables de prueba para probar cada escenario que necesita admitir.
Me gusta escribir pruebas unitarias que usan uno de los serializadores integrados en el objeto original y el clonado y luego verificar las representaciones serializadas para determinar la igualdad (para un formateador binario, puedo simplemente comparar las matrices de bytes). Esto funciona muy bien en los casos en que el objeto todavía es serializable, y solo estoy cambiando a un clon profundo personalizado por razones de rendimiento.
Además, me gusta agregar una verificación de modo de depuración a todas mis implementaciones de clonación usando algo como esto
[Conditional("DEBUG")]
public static void DebugAssertValueEquality<T>(T current, T other, bool expected,
params string[] ignoredFields) {
if (null == current)
{ throw new ArgumentNullException("current"); }
if (null == ignoredFields)
{ ignoredFields = new string[] { }; }
FieldInfo lastField = null;
bool test;
if (object.ReferenceEquals(other, null))
{ Debug.Assert(false == expected, "The other object was null"); return; }
test = true;
foreach (FieldInfo fi in current.GetType().GetFields(BindingFlags.Instance)) {
if (test = false) { break; }
if (0 <= Array.IndexOf<string>(ignoredFields, fi.Name))
{ continue; }
lastField = fi;
object leftValue = fi.GetValue(current);
object rightValue = fi.GetValue(other);
if (object.ReferenceEquals(null, leftValue)) {
if (!object.ReferenceEquals(null, rightValue))
{ test = false; }
}
else if (object.ReferenceEquals(null, rightValue))
{ test = false; }
else {
if (!leftValue.Equals(rightValue))
{ test = false; }
}
}
Debug.Assert(test == expected, string.Format("field: {0}", lastField));
}
Este método se basa en una implementación precisa de Equals en cualquier miembro anidado, pero en mi caso todo lo que es clonable es también equitable
Hay una solución realmente obvia que no requiere tanto trabajo:
- Serializar el objeto en un formato binario
- Clona el objeto.
- Serialice el clon en un formato binario.
- Compara los bytes.
Suponiendo que la serialización funciona, y es mejor porque la está usando para clonar, esto debería ser fácil de mantener. De hecho, estará encapsulado por completo de los cambios a la estructura de su clase.
Normalmente implementaría Equals()
para comparar los dos objetos en profundidad. Puede que no lo necesite en su código de producción, pero aún podría ser útil más adelante y el código de prueba es mucho más limpio.
Aquí hay una muestra de cómo implementé esto hace un tiempo, aunque esto tendrá que adaptarse al escenario. En este caso, teníamos una desagradable cadena de objetos que podía cambiar fácilmente y el clon se usó como una implementación de prototipo muy crítica, así que tuve que parchar (hackear) esta prueba juntos.
public static class TestDeepClone
{
private static readonly List<long> objectIDs = new List<long>();
private static readonly ObjectIDGenerator objectIdGenerator = new ObjectIDGenerator();
public static bool DefaultCloneExclusionsCheck(Object obj)
{
return
obj is ValueType ||
obj is string ||
obj is Delegate ||
obj is IEnumerable;
}
/// <summary>
/// Executes various assertions to ensure the validity of a deep copy for any object including its compositions
/// </summary>
/// <param name="original">The original object</param>
/// <param name="copy">The cloned object</param>
/// <param name="checkExclude">A predicate for any exclusions to be done, i.e not to expect IPolicy items to be cloned</param>
public static void AssertDeepClone(this Object original, Object copy, Predicate<object> checkExclude)
{
bool isKnown;
if (original == null) return;
if (copy == null) Assert.Fail("Copy is null while original is not", original, copy);
var id = objectIdGenerator.GetId(original, out isKnown); //Avoid checking the same object more than once
if (!objectIDs.Contains(id))
{
objectIDs.Add(id);
}
else
{
return;
}
if (!checkExclude(original))
{
Assert.That(ReferenceEquals(original, copy) == false);
}
Type type = original.GetType();
PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
FieldInfo[] fieldInfos = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo memberInfo in propertyInfos)
{
var getmethod = memberInfo.GetGetMethod();
if (getmethod == null) continue;
var originalValue = getmethod.Invoke(original, new object[] { });
var copyValue = getmethod.Invoke(copy, new object[] { });
if (originalValue == null) continue;
if (!checkExclude(originalValue))
{
Assert.That(ReferenceEquals(originalValue, copyValue) == false);
}
if (originalValue is IEnumerable && !(originalValue is string))
{
var originalValueEnumerable = originalValue as IEnumerable;
var copyValueEnumerable = copyValue as IEnumerable;
if (copyValueEnumerable == null) Assert.Fail("Copy is null while original is not", new[] { original, copy });
int count = 0;
List<object> items = copyValueEnumerable.Cast<object>().ToList();
foreach (object o in originalValueEnumerable)
{
AssertDeepClone(o, items[count], checkExclude);
count++;
}
}
else
{
//Recurse over reference types to check deep clone success
if (!checkExclude(originalValue))
{
AssertDeepClone(originalValue, copyValue, checkExclude);
}
if (originalValue is ValueType && !(originalValue is Guid))
{
//check value of non reference type
Assert.That(originalValue.Equals(copyValue));
}
}
}
foreach (FieldInfo fieldInfo in fieldInfos)
{
var originalValue = fieldInfo.GetValue(original);
var copyValue = fieldInfo.GetValue(copy);
if (originalValue == null) continue;
if (!checkExclude(originalValue))
{
Assert.That(ReferenceEquals(originalValue, copyValue) == false);
}
if (originalValue is IEnumerable && !(originalValue is string))
{
var originalValueEnumerable = originalValue as IEnumerable;
var copyValueEnumerable = copyValue as IEnumerable;
if (copyValueEnumerable == null) Assert.Fail("Copy is null while original is not", new[] { original, copy });
int count = 0;
List<object> items = copyValueEnumerable.Cast<object>().ToList();
foreach (object o in originalValueEnumerable)
{
AssertDeepClone(o, items[count], checkExclude);
count++;
}
}
else
{
//Recurse over reference types to check deep clone success
if (!checkExclude(originalValue))
{
AssertDeepClone(originalValue, copyValue, checkExclude);
}
if (originalValue is ValueType && !(originalValue is Guid))
{
//check value of non reference type
Assert.That(originalValue.Equals(copyValue));
}
}
}
}
}