Допустим, у меня есть сложный класс .NET с большим количеством массивов и других членов объекта класса. Мне нужно иметь возможность генерировать глубокий клон этого объекта - поэтому я пишу метод Clone() и реализую его с помощью простого BinaryFormatter serialize/deserialize - или, возможно, я делаю глубокий клон с помощью какой-то другой техники, которая более подвержена ошибкам, и я хотел бы убедиться, что она проверена.
Хорошо, теперь (хорошо, я должен был сделать это первым) я хочу написать тесты, которые охватывают клонирование. Все члены класса приватны, и моя архитектура настолько хороша (!), что мне не пришлось писать сотни публичных свойств или других аксессоров. Класс не является IComparable или IEquatable, потому что это не нужно приложению. Мои юнит-тесты находятся в отдельной сборке от производственного кода.
Какие подходы используют люди для проверки того, что клонированный объект является хорошей копией? Пишете ли вы (или переписываете, когда обнаруживаете необходимость в клонировании) все ваши модульные тесты для класса так, чтобы их можно было вызывать либо с 'девственным' объектом, либо с его клоном? Как вы будете проверять, если часть клонирования не была достаточно глубокой - ведь это как раз та проблема, которая может дать скрытые и трудно находимые ошибки позже?
Метод тестирования будет зависеть от типа решения, которое вы придумали. Если вы пишете пользовательский код клонирования и должны вручную реализовать его в каждом клонируемом типе, тогда вам действительно следует протестировать клонирование каждого из этих типов. В противном случае, если вы решите пойти более общим путем (где вышеупомянутое отражение, скорее всего, будет уместно), ваши тесты должны будут проверить только конкретные сценарии, с которыми придется иметь дело вашей системе клонирования.
Чтобы ответить на ваши конкретные вопросы:
Пишете ли вы (или переписываете, когда обнаруживаете необходимость в клонировании) все ваши модульные тесты для класса так, чтобы их можно было вызывать либо с 'девственным' объектом, либо с его клоном?
У вас должны быть тесты для всех методов, которые могут быть выполнены как на оригинальном, так и на клонированном объекте. Обратите внимание, что создать простой дизайн тестов для поддержки этого довольно легко без ручного обновления логики для каждого теста.
Как вы будете тестировать, если часть клонирования не была достаточно глубокой - ведь это как раз та проблема, которая может дать скрытые ошибки впоследствии?
Это зависит от выбранного вами метода клонирования. Если вам придется вручную обновлять клонируемые типы, то вы должны проверить, что каждый тип клонирует все (и только) члены, которые вы ожидаете. Если же вы тестируете фреймворк для клонирования, я бы создал несколько тестовых клонируемых типов для проверки каждого сценария, который вам нужно поддерживать.
There' s действительно очевидное решение это doesn' t берут почти в качестве большой работы:
Предположение, что преобразование в последовательную форму работает - и это лучше, потому что Вы используете его, чтобы клонироваться - это должно быть легко поддержать. На самом деле это будет заключено в капсулу от изменений до структуры Вашего класса полностью.
Я бы просто написал один тест, чтобы определить, был ли клон правильным или нет. Если класс не запечатан, вы можете создать для него обвязку, расширив его, а затем раскрыв все свои внутренние компоненты в дочернем классе. В качестве альтернативы можно использовать отражение (yech), или использовать генераторы аксессоров MSTest'.
Вам нужно клонировать объект, а затем просмотреть все свойства и переменные, которые есть у объекта, и определить, правильно ли они были скопированы или клонированы.
Мне нравится писать модульные тесты, которые используют один из встроенных сериализаторов для исходного и клонированного объекта, а затем проверяют сериализованные представления на равенство (для двоичного форматера я могу просто сравнить байтовые массивы). Это отлично работает в случаях, когда объект все еще сериализуем, и я перехожу на собственный глубокий клон только из соображений совершенства.
Более того, мне нравится добавлять проверку режима отладки во все мои реализации Clone, используя что-то вроде этого
[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));
}
Этот метод зависит от точной реализации Equals для всех вложенных членов, но в моем случае все, что можно клонировать, также можно приравнять.
Я обычно осуществлял бы, 'Равняется ()' для сравнения двух объектов подробно. Вам, возможно, не понадобился бы он в Вашем производственном кодексе, но это могло бы все еще пригодиться позже, и испытательный кодекс намного более чистый.
Вот образец того, как я осуществил это некоторое время назад, хотя это должно будет быть скроено к сценарию. В этом случае у нас была противная цепь объекта, которая могла легко измениться, и клон использовался в качестве очень критического внедрения прототипа и таким образом, я должен был исправить (взламывают) этот тест вместе.
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));
}
}
}
}
}