domingo, 4 de octubre de 2020

Test para comprobar de que todos los métodos de los servicios están anotados con @Transactional


Si bien es medio viejo esto, lo use hace poco en un proyecto (tambien medio viejo) pero pensé que les podría ser útil. 

Un método común para establecer límites de transacciones en Spring Framework es utilizar su gestión de transacciones impulsada por anotaciones y anotar métodos de servicio con la anotación @Transactional. Parece bastante simple, ¿verdad? Si y no. Aunque la gestión de transacciones impulsada por anotaciones de Spring Framework es fácil de configurar y usar, hay algunas cosas que debe recordar hacer:

  • Debe recordar anotar cada método de servicio con la anotación @Transactional. Esto puede parecer una tarea fácil, pero como probablemente eres un ser humano, también eres capaz de cometer errores. Un error como este podría dejar la base de datos de su aplicación en un estado inconsistente si algo sale mal mientras su aplicación escribe información en la base de datos.
  • Si desea revertir la transacción cuando un método de servicio arroja una excepción marcada, debe especificar la clase de excepción marcada lanzada como un valor de la propiedad rollbackFor de la anotación @Transactional. Esto es necesario porque, por defecto, Spring Framework no revertirá la transacción cuando se lanza una excepción marcada. Si el atributo rollbackFor es de la anotación @Transactional no está configurado y se lanza una excepción marcada cuando su aplicación está escribiendo información en la base de datos, la base de datos de su aplicación podría terminar en un estado inconsistente.

Afortunadamente, es bastante fácil implementar una prueba que garantice qu

  • Cada método de una clase de servicio, excepto los getter y setter, se anota con una anotación @Transactional.
  • Cada excepción checked que lanza un método de servicio se establece como un valor de la propiedad rollbackFor de la anotación @Transactional.
  • Como beneficio adicional, esta prueba también verificará que cada clase de servicio esté anotada con la anotación @Service.

Sin más el test : 

public class ServiceAnnotationTest {

 

    private static final String PACKAGE_PATH_SEPARATOR = ".";

 

    /*

     * A string which is used to identify getter methods. All methods whose name contains the given string

     * are considered as getter methods.

     */

    private static final String GETTER_METHOD_NAME_ID = "get";

    private static final String FILE_PATH_SEPARATOR = System.getProperty("file.separator");

 

    /*

     * The file path to the root folder of service package. If the absolute path to the service package

     * is /users/foo/classes/com/bar/service and the classpath base directory is /users/foo/classes,

     * the value of this constant must be /com/bar/service.

     */

    private static final String SERVICE_BASE_PACKAGE_PATH = "/com/bar/service";

 

    /*

     * A string which is used to identify setter methods. All methods whose name contains the given string

     * are considered as setter methods.

     */

    private static final String SETTER_METHOD_NAME_ID = "set";

 

    /*

     * A string which is used to identify the test classes. All classes whose name contains the given string

     * are considered as test classes.

     */

    private static final String TEST_CLASS_FILENAME_ID = "Test";

 

    private List<Class> serviceClasses;

 

    /**

     * Iterates through all the classes found under the service base package path (and its sub directories)

     * and inserts all service classes to the serviceClasses array.

     *

     * @throws IOException

     * @throws ClassNotFoundException

     */

    @Before

    public void findServiceClasses() throws IOException, ClassNotFoundException {

        serviceClasses = new ArrayList<Class>();

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

        Resource[] resources = resolver.getResources("classpath*:" + SERVICE_BASE_PACKAGE_PATH + "/**/*.class");

        for (Resource resource : resources) {

            if (isNotTestClass(resource)) {

                String serviceClassCandidateNameWithPackage = parseClassNameWithPackage(resource);

                ClassLoader classLoader = resolver.getClassLoader();

                Class serviceClassCandidate = classLoader.loadClass(serviceClassCandidateNameWithPackage);

                if (isNotInterface(serviceClassCandidate)) {

                    if (isNotException(serviceClassCandidate)) {

                        if (isNotEnum(serviceClassCandidate)) {

                            if (isNotAnonymousClass(serviceClassCandidate)) {

                                serviceClasses.add(serviceClassCandidate);

                            }

                        }

                    }

                }

            }

        }

    }

 

    /**

     * Checks if the resource given a as parameter is a test class. This method returns

     * true if the resource is not a test class and false otherwise.

     *

     * @param resource

     * @return

     */

    private boolean isNotTestClass(Resource resource) {

        return !resource.getFilename().contains(TEST_CLASS_FILENAME_ID);

    }

 

    /**

     * Checks if the resource given as a parameter is an exception class. This method returns true

     * if the class is not an exception class and false otherwise.

     *

     * @param exceptionCanditate

     * @return

     */

    private boolean isNotException(Class exceptionCanditate) {

        return !Exception.class.isAssignableFrom(exceptionCanditate) &&

                !RuntimeException.class.isAssignableFrom(exceptionCanditate) &&

                !Throwable.class.isAssignableFrom(exceptionCanditate);

    }

 

    /**

     * Parses a class name from the absolute path of the resource given as a parameter

     * and returns the parsed class name. E.g. if the absolute path of the resource is

     * /user/foo/classes/com/foo/Bar.class, this method returns com.foo.Bar.

     *

     * @param resource

     * @return

     * @throws IOException

     */

    private String parseClassNameWithPackage(Resource resource) throws IOException {

        String pathFromClasspathRoot = parsePathFromClassPathRoot(resource.getFile().getAbsolutePath());

        String pathWithoutFilenameSuffix = parsePathWithoutFilenameSuffix(pathFromClasspathRoot);

        return buildClassNameFromPath(pathWithoutFilenameSuffix);

    }

 

    /**

     * Parses the path which starts from the classpath root directory by using the

     * absolute path given as a parameter. Returns the parsed path.

     * E.g. If the absolute path is /user/foo/classes/com/foo/Bar.class and the classpath

     * root directory is /user/foo/classes/, com/foo/Bar.class is returned.

     *

     * @param absolutePath

     * @return

     */

    private String parsePathFromClassPathRoot(String absolutePath) {

        int classpathRootIndex = absolutePath.indexOf(SERVICE_BASE_PACKAGE_PATH);

        return absolutePath.substring(classpathRootIndex + 1);

    }

 

    /**

     * Removes the file suffix from the path given as a parameter and returns new path

     * without the suffix. E.g. If path is com/foo/Bar.class, com/foo/Bar is returned.

     *

     * @param path

     * @return

     */

    private String parsePathWithoutFilenameSuffix(String path) {

        int prefixIndex = path.indexOf(PACKAGE_PATH_SEPARATOR);

        return path.substring(0, prefixIndex);

    }

 

    /**

     * Builds a class name with package information from a path given as a parameter and

     * returns the class name with package information. e.g. If a path com/foo/Bar is given

     * as a parameter, com.foo.Bar is returned.

     *

     * @param path

     * @return

     */

    private String buildClassNameFromPath(String path) {

        return path.replace(FILE_PATH_SEPARATOR, PACKAGE_PATH_SEPARATOR);

    }

 

    /**

     * Checks if the class given as an argument is an interface or not.

     * Returns false if the class is not an interface and true otherwise.

     *

     * @param interfaceCanditate

     * @return

     */

    private boolean isNotInterface(Class interfaceCanditate) {

        return !interfaceCanditate.isInterface();

    }

 

    /**

     * Checks if the class given as an argument is an Enum or not.

     * Returns false if the class is not Enum and true otherwise.

     *

     * @param enumCanditate

     * @return

     */

    private boolean isNotEnum(Class enumCanditate) {

        return !enumCanditate.isEnum();

    }

 

    /**

     * Checks if the class given as a parameter is an anonymous class.

     * Returns true if the class is not an anonymous class and false otherwise.

     *

     * @param anonymousClassCanditate

     * @return

     */

    private boolean isNotAnonymousClass(Class anonymousClassCanditate) {

        return !anonymousClassCanditate.isAnonymousClass();

    }

 

    /**

     * Verifies that each method which is declared in a service class and which is not a

     * getter or setter method is annotated with Transactional annotation. This test

     * also ensures that the rollbackFor property of Transactional annotation specifies

     * all checked exceptions which are thrown by the service method.

     */

    @Test

    public void eachServiceMethodHasTransactionalAnnotation() {

        for (Class serviceClass : serviceClasses) {

            Method[] serviceMethods = serviceClass.getMethods();

            for (Method serviceMethod : serviceMethods) {

                if (isMethodDeclaredInServiceClass(serviceMethod, serviceClass)) {

                    if (isNotGetterOrSetterMethod(serviceMethod)) {

                        boolean transactionalAnnotationFound = serviceMethod.isAnnotationPresent(Transactional.class);

                        assertTrue("Method " + serviceMethod.getName() + " of " + serviceClass.getName() + " class must be annotated with @Transactional annotation.", transactionalAnnotationFound);

                        if (transactionalAnnotationFound) {

                            if (methodThrowsCheckedExceptions(serviceMethod)) {

                                boolean rollbackPropertySetCorrectly = rollbackForPropertySetCorrectlyForTransactionalAnnotation(serviceMethod.getAnnotation(Transactional.class), serviceMethod.getExceptionTypes());

                                assertTrue("Method " + serviceMethod.getName() + "() of " + serviceClass.getName() + " class must set rollbackFor property of Transactional annotation correctly", rollbackPropertySetCorrectly);

                            }

                        }

                    }

                }

            }

        }

    }

 

    /**

     * Checks that the method given as a parameter is declared in a service class given as

     * a parameter. Returns true if the method is declated in service class and false

     * otherwise.

     *

     * @param method

     * @param serviceClass

     * @return

     */

    private boolean isMethodDeclaredInServiceClass(Method method, Class serviceClass) {

        return method.getDeclaringClass().equals(serviceClass);

    }

 

    /**

     * Checks if the method given as parameter is a getter or setter method. Returns true

     * if the method is a getter or setter method an false otherwise.

     *

     * @param method

     * @return

     */

    private boolean isNotGetterOrSetterMethod(Method method) {

        return !method.getName().contains(SETTER_METHOD_NAME_ID) && !method.getName().contains(GETTER_METHOD_NAME_ID);

    }

 

    /**

     * Checks if the method given as a parameter throws checked exceptions. Returns true

     * if the method throws checked exceptions and false otherwise.

     *

     * @param method

     * @return

     */

    private boolean methodThrowsCheckedExceptions(Method method) {

        return method.getExceptionTypes().length > 0;

    }

 

    /**

     * Checks if the transactional annotation given as a parameter specifies all checked exceptions

     * given as a parameter as a value of rollbackFor property. Returns true if all exceptions

     * are specified and false otherwise.

     *

     * @param annotation

     * @param thrownExceptions

     * @return

     */

    private boolean rollbackForPropertySetCorrectlyForTransactionalAnnotation(Annotation annotation, Class<?>[] thrownExceptions) {

        boolean rollbackForSet = true;

 

        if (annotation instanceof Transactional) {

            Transactional transactional = (Transactional) annotation;

            List<Class<? extends Throwable>> rollbackForClasses = Arrays.asList(transactional.rollbackFor());

            for (Class<?> thrownException : thrownExceptions) {

                if (!rollbackForClasses.contains(thrownException)) {

                    rollbackForSet = false;

                    break;

                }

            }

        }

 

        return rollbackForSet;

    }

 

    /**

     * Verifies that each service class is annotated with @Service annotation.

     */

    @Test

    public void eachServiceClassIsAnnotatedWithServiceAnnotation() {

        for (Class serviceClass : serviceClasses) {

            assertTrue(serviceClass.getSimpleName() + " must be annotated with @Service annotation", serviceClass.isAnnotationPresent(Service.class));

        }

    }

}

Dejo link : https://www.petrikainulainen.net/programming/tips-and-tricks/testing-that-all-service-methods-are-annotated-with-transactional-annotation/



No hay comentarios.:

Publicar un comentario