Модульное тестирование сериализуемости для всех классов в java project

#java #unit-testing #serialization

#java #модульное тестирование #сериализация

Вопрос:

У меня тысячи классов в нашем java-проекте. Некоторые из них реализуют сериализуемый интерфейс. Теперь вот проблема. Возможно, кто-то может зайти в класс, добавить новую переменную, которая не является ни переходной, ни сериализуемой. Код компилируется нормально, однако процесс будет взорван во время выполнения.

Чтобы проиллюстрировать это

 class Foo implements Serializable {  .... // all good }

class Foo implements Serializable 
{  
    // OOps, executorService is not serializable.  It's not declared as transient either 

    private ExecutorService executorService = ..
}
  

Я подумываю о написании модульного теста, который проходил бы через все классы и обеспечивал «истинную сериализуемость». Я прочитал несколько дискуссий о сериализации конкретных объектов. я понимаю этот процесс, но он требует

1) создание объекта.
2) сериализация, а затем
3) десериализация.

Существует ли более эффективный и практичный подход. возможно, использовать отражение. Пройдите через все классы, если класс имеет serializable, тогда все атрибуты должны быть сериализуемыми или иметь ключевое слово transient..

Мысли?

Ответ №1:

1) создание объекта. 2) сериализация, а затем 3) десериализация.

Этот список неполный; вам также потребуется инициализация. Рассмотрим пример:

 class CanBeSerialized implements Serializable {
    private String a; // serializable
    private Thread t; // not serializable
}

class CannotBeSerialized implements Serializable {
    private String a;                // serializable
    private Thread t = new Thread(); // not serializable
}
  

Вы можете сериализовать и десериализовать первый, но вы перейдете NotSerializableException ко второму. Чтобы еще больше усложнить ситуацию, если используются интерфейсы, вы никогда не сможете сказать, пройдет ли класс сериализацию, поскольку потоковым будет конкретный объект класса, стоящий за этим интерфейсом:

 class PerhapsCanBeSerializedButYouNeverKnow implements Serializable {
    private Runnable r; // interface type - who knows?
}
  

При условии, что вы можете гарантировать следующее для всех ваших классов и классов, используемых вашими классами для тестирования:

  • конструктор по умолчанию существует,
  • нет типов интерфейса в полях,

затем вы можете автоматически создавать и инициализировать их путем отражения, а затем тестировать сериализацию. Но это действительно сложное условие, не так ли? В противном случае правильная инициализация сводится к ручной работе.

Вы могли бы использовать отражение по-другому: перебирать список Class объектов, которые вы хотите проверить, получать Field[] для них и проверять, являются ли они переходными ( Field.getModifiers() ) или реализуются ли они Serializable напрямую ( Field.getType().getInterfaces() ) или косвенно (через суперинтерфейс или класс). Кроме того, подумайте, насколько глубоко вы хотите проверить, в зависимости от того, насколько глубоко работает ваш механизм сериализации.

Как правильно указал Райан, эта проверка статической сериализации завершилась бы неудачей, если бы код был достаточно злым:

 class SeeminglySerializable implements Serializable {
    // ...
        private void writeObject/readObject() {
             throw new NotSerializableException();
        }
}
  

или просто, если readObject()/writeObject() они были плохо реализованы. Чтобы протестировать такого рода проблемы, вам нужно фактически протестировать процесс сериализации, а не код, стоящий за ним.

Комментарии:

1. Вы все еще не учли волшебные методы readObject и writeObject . Если они будут использованы, ни одно из перечисленных вами правил не будет применяться.

2. @RyanStewart Я собирался упомянуть, что мой алгоритм является лишь приближением, но он ускользнул от меня. Спасибо!

Ответ №2:

Если сериализация является ключевой частью вашего приложения, включите сериализацию в свои тесты. Что-то вроде:

 @Test
public void aFooSerializesAndDeserializesCorrectly {
    Foo fooBeforeSerialization = new Foo();
    ReflectionUtils.randomlyPopulateFields(foo);
    Foo fooAfterSerialization = Serializer.serializeAndDeserialize(foo);
    assertThat(fooAfterSerialization, hasSameFieldValues(fooBeforeSerialization));
}
  

Редактировать: тривиальная реализация randomlyPopulateFields :

 public static void randomlyPopulateFields(final Object o) {
    ReflectionUtils.doWithFields(o.getClass(), new ReflectionUtils.FieldCallback() {
        public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
            ReflectionUtils.makeAccessible(field);
            ReflectionUtils.setField(field, o, randomValueFor(field.getType()));
        }

        private Random r = new Random();
        private Object randomValueFor(Class<?> type) {
            if (type == String.class) {
                return String.valueOf(r.nextDouble());
            } else if (type == Boolean.class || type == Boolean.TYPE) {
                return r.nextBoolean();
            } else if (type == Byte.class || type == Byte.TYPE) {
                return (byte) r.nextInt();
            } else if (type == Short.class || type == Short.TYPE) {
                return (short) r.nextInt();
            } else if (type == Integer.class || type == Integer.TYPE) {
                return r.nextInt();
            } else if (type == Long.class || type == Long.TYPE) {
                return (long) r.nextInt();
            } else if (Number.class.isAssignableFrom(type) || type.isPrimitive()) {
                return Byte.valueOf("1234");
            } else if (Date.class.isAssignableFrom(type)) {
                return new Date(r.nextLong());
            } else {
                System.out.println("Sorry, I don't know how to generate values of type "   type);
                return null;
            }
        }
    });
}
  

Комментарии:

1. Что ReflectionUtils вы имеете в виду?

2. Ничего особенного. Я думаю, что где-то видел подобную утилиту в какой-то библиотеке, но обычно я просто пишу свою собственную. Любой способ создания надлежащим образом заполненного объекта будет работать. Дело в том, что если вы собираетесь тестировать сериализацию, то вам нужно сериализовать объекты.

3. Проблема в том, что такой метод не может быть тривиально реализован без предоставления метаданных о том, как инициализировать объект. Вот почему я нахожу ваш ответ вводящим в заблуждение, поскольку он обманывает, казалось бы, простое решение проблемы.

4. Добавлена простая реализация. Хуже всего то, что приходится иметь дело со всеми числовыми типами по отдельности. Это не очень сложно. Это также только один из возможных способов получения заполненных объектов для тестов. Фактический подход будет зависеть от того, какую тестовую инфраструктуру вы уже создали.

5. Отлично. Теперь вы ясно показываете, что за вашим методом нет никакой магии, только тяжелая работа.

Ответ №3:

Поместите классы, которые вы хотите протестировать, внутри метода initParameters() и запустите весь класс как JUnit test.

 import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static java.lang.reflect.Modifier.FINAL;
import static java.lang.reflect.Modifier.STATIC;
import static java.lang.reflect.Modifier.TRANSIENT;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

@SuppressWarnings({"rawtypes"})
@RunWith(Parameterized.class)
public class SerializableTest {
    @Parameterized.Parameter
    public Class clazz;

    @Parameterized.Parameters(name = "{0}")
    public static Collection<Object[]> initParameters() {
        return Arrays.asList(new Object[][]{
                // TODO put your classes here
                {YourClassOne.class},
                {YourClassTwo.class},
        });
    }

    @Test
    @SuppressWarnings("Convert2Diamond")
    public void testSerializableHierarchy() throws ReflectiveOperationException {
        performTestSerializableHierarchy(new TreeMap<Class, Boolean>(new Comparator<Class>() {
            @Override
            public int compare(Class o1, Class o2) {
                return o1.getName().compareTo(o2.getName());
            }
        }), new HashMap<Long, Class>(), clazz);
    }

    @SuppressWarnings("ConstantConditions")
    private void performTestSerializableHierarchy(Map<Class, Boolean> classes, Map<Long, Class> uids, Type type) throws IllegalAccessException {
        if (type instanceof GenericArrayType) {
            performTestSerializableHierarchy(classes, uids, ((GenericArrayType) type).getGenericComponentType());
            return;
        } else if (type instanceof ParameterizedType) {
            performTestSerializableHierarchy(classes, uids, ((ParameterizedType) type).getRawType());

            Type[] types = ((ParameterizedType) type).getActualTypeArguments();
            for (Type parameterType : types) {
                performTestSerializableHierarchy(classes, uids, parameterType);
            }
            return;
        } else if (!(type instanceof Class)) {
            fail("Invalid type: "   type);
            return;
        }

        Class clazz = (Class) type;

        if (clazz.isPrimitive()) {
            return;
        }

        if (clazz.isEnum()) {
            return;
        }

        if (clazz.equals(String.class)) {
            return;
        }

        if (clazz.isArray()) {
            performTestSerializableHierarchy(classes, uids, clazz.getComponentType());
            return;
        }

        if (Collection.class.isAssignableFrom(clazz)) {
            return;
        }

        if (Map.class.isAssignableFrom(clazz)) {
            return;
        }

        if (!Serializable.class.isAssignableFrom(clazz)) {
            fail(clazz   " does not implement "   Serializable.class.getSimpleName());
            return;
        }

        Boolean status = classes.get(clazz);
        if (status == null) {
            classes.put(clazz, false);
        } else if (status) {
            return;
        }

        Field uidField = null;

        try {
            uidField = clazz.getDeclaredField("serialVersionUID");
        } catch (NoSuchFieldException ex) {
            fail(clazz   " does not declare field 'serialVersionUID'");
        }

        assertNotNull(uidField);

        if ((uidField.getModifiers() amp; (STATIC | FINAL)) != (STATIC | FINAL) || !uidField.getType().equals(long.class)) {
            fail(clazz   " incorrectly declares field 'serialVersionUID'");
        }

        uidField.setAccessible(true);
        long uidValue = (long) uidField.get(null);
        if (uidValue == ((int) uidValue)) {
            fail(uidField   " has invalid value: "   uidValue);
        }

        Class existingClass = uids.get(uidValue);
        if (existingClass != null amp;amp; !existingClass.equals(clazz)) {
            fail(existingClass   " has assigned 'serialVersionUID' same value as "   clazz);
        }

        for (Field field : clazz.getDeclaredFields()) {
            if ((field.getModifiers() amp; (STATIC | TRANSIENT)) == 0) {
                performTestSerializableHierarchy(classes, uids, field.getGenericType());
            }
        }

        classes.put(clazz, true);
        uids.put(uidValue, clazz);
    }
}

  

Ответ №4:

этот модульный тест находит все классы, packageName которые являются потомками Serializable , и проверяет объявленные свойства этих классов, чтобы увидеть, являются ли они также потомками Serializable .

зависимости библиотеки

 dependencies {
    implementation 'io.github.classgraph:classgraph:4.8.151'
}
  

модульный тест

 import io.github.classgraph.ClassGraph
import org.junit.Test
import java.io.Serializable
import java.util.LinkedHashSet
import kotlin.reflect.KClass
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.jvm.javaField

class SerializableTest
{
    @Test
    fun `all properties of Serializable descendants are also Serializable descendants`()
    {
        // change BuildConfig.APPLICATION_ID to be root package of your project, unless you are also android developer! :D
        val packageName = BuildConfig.APPLICATION_ID

        // this should be okay to keep it as it is as long as you put it in the same package as root of your project
        val classLoader = SerializableTest::class.java.classLoader!!
        val allClasses = tryGetClassesForPackage(packageName,classLoader)
        val nonSerializableMembersOfSerializableSequence = allClasses
            .asSequence()
            .onEach { jClass -> println("found class in package $packageName: $jClass") }
            .filter { jClass -> Serializable::class.java.isAssignableFrom(jClass) }
            .onEach { serializableJClass -> println("found Serializable subclass: $serializableJClass") }
            .filter { serializableJClass -> serializableJClass.kotlin.simpleName != null }
            .onEach { serializableJClass -> println("found non-anonymous Serializable subclass: $serializableJClass") }
            .flatMap()
            { serializableJClass ->
                serializableJClass.kotlin.simpleName
                serializableJClass.kotlin.declaredMemberProperties.asSequence()
                    .onEach { property -> println("found property of Serializable subclass: ${property.name}") }
                    .filter { property -> property.javaField != null }
                    .onEach { property -> println("found java-field-backed property of Serializable subclass: ${property.name}") }
                    .mapNotNull()
                    { kProperty ->
                        val propertyClass = kProperty.returnType.classifier as? KClass<*>
                        val isMemberNonSerializable = propertyClass?.isSubclassOf(Serializable::class) != true
                        if (isMemberNonSerializable) serializableJClass.canonicalName to kProperty.name else null
                    }
            }
        val nonSerializableMembersOfSerializable = nonSerializableMembersOfSerializableSequence.toSet()
        nonSerializableMembersOfSerializable.onEach { serializableJClass ->
            val serializableClassName = serializableJClass.first
            val nonSerializablePropertyName = serializableJClass.second
            System.err.println("found Serializable subclass ($serializableClassName) with non-Serializable member: $nonSerializablePropertyName")
        }
        assert(nonSerializableMembersOfSerializable.isEmpty())
    }

    private fun tryGetClassesForPackage(packageName:String,loader:ClassLoader):Sequence<Class<*>>
    {
        return ClassGraph().acceptPackages(packageName).enableClassInfo().scan().use { scanResult ->
            scanResult.allClasses.names.asSequence().map { className -> loader.loadClass(className) }
        }
    }
}