Java项目中所有类的单元测试序列化

mzsu5hc0  于 2022-12-10  发布在  Java
关注(0)|答案(4)|浏览(111)

我的java项目中有上千个类。其中一些实现了可序列化的接口。现在这里有一个问题。可能有人可以进入一个类,添加一个既不是 transient 也不是可序列化的新变量。代码编译得很好,但是进程在运行时会崩溃。
为了说明这一点

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)正在还原序列化。
是否有更有效和实用的方法。也许使用反射。检查所有类,如果类有可序列化的,那么所有属性都必须是可序列化的或有 transient 关键字。
有什么想法?

luaexgnf

luaexgnf1#

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的具体对象,将被流传输:

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

如果您可以保证所有类和类使用的类都满足以下条件:

  • 默认构造函数存在,
  • 字段中没有接口类型,

然后测试序列化。但这确实是一个很难的条件,不是吗?否则,正确的初始化就需要手工操作了。
您可以用不同的方式使用反映:遍历要检查的Class对象列表,获取它们的Field[],并验证它们是 transient 的(Field.getModifiers()),还是直接(Field.getType().getInterfaces())或间接(通过超接口或类)实现Serializable
正如Ryan正确指出的那样,如果代码足够邪恶,这种静态序列化检查就会失败:

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

或者仅仅是readObject()/writeObject()的实现不好。要针对这类问题进行测试,您需要实际测试序列化过程,而不是它背后的代码。

xv8emn3q

xv8emn3q2#

如果序列化是应用的关键部分,那么在测试中包括序列化。

@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;
            }
        }
    });
}
z31licg0

z31licg03#

将您要测试的类放入initParameters()方法中,并将整个类作为JUnit测试运行。

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() & (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 && !existingClass.equals(clazz)) {
            fail(existingClass + " has assigned 'serialVersionUID' same value as " + clazz);
        }

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

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

yrefmtwq4#

这个单元测试在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) }
        }
    }
}

相关问题