2 votes

Le remplacement du constructeur d'une métaclasse ne fonctionne pas pour une méthode à l'intérieur d'une classe annotée @CompileStatic

Lorsque nous utilisons someClass.metaClass.constructor pour une classe spécifique (comme RESTClient ) disponible à l'intérieur de la méthode d'une classe qui est annotée avec @CompileStatic Le remplacement du constructeur ne fonctionne pas du tout.

Quand nous avons retiré le @CompileStatic cela fonctionne correctement. Est-ce que quelque chose m'échappe ?

Code échantillon :

@CompileStatic
class FooClass {

    String getDataFromProvider() {
        String url = "https://www.example.com"
        RESTClient restClient = new RESTClient(url)

        HttpResponseDecorator response = restClient.post([:]) as HttpResponseDecorator
        return response
    }
}

Et le cas d'essai :

import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.RESTClient
import spock.lang.Specification

class FooContentSpec extends Specification {

    void "test getDataFromProvider method"() {
        given: "Rest url"
        String restURL = "https://www.example.com"

        and: "Mock RESTClient"
        RESTClient mockedRestClient = Mock(RESTClient)

        // THIS IS NOT WORKING
        RESTClient.metaClass.constructor = { Object url ->
            assert restURL == url
            return mockedRestClient
        }

        mockedRestClient.metaClass.post = { Map<String, ?> args ->
            return ""
        }

        when: "We hit the method"
        HttpResponseDecorator response = Content.getDataFromProvider()

        then: "We should get status 200"
        response.statusCode == 200
    }
}

Selon le Groovy Lang doc :

MockFor y StubFor ne peut pas être utilisé pour tester des classes compilées statiquement, par exemple pour les classes Java ou Groovy qui font appel à @CompileStatic . Pour bloquer et/ou simuler ces classes, vous pouvez utiliser Spock ou l'une des bibliothèques de simulation Java.

Comportement attendu

Dans ce scénario, la surcharge du constructeur de l'option RESTClient devrait fonctionner dans nos scénarios de test car nous ne voulons pas utiliser l'API d'un tiers dans chaque scénario de test.

Comportement réel

Malheureusement, RESTClient ne fait pas l'objet de moqueries à cause de @CompileStatic il frappe l'API à chaque fois.

Informations sur l'environnement

------------------------------------------------------------
Gradle 3.5
------------------------------------------------------------

Groovy:       2.4.10,
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015,
JVM:          1.8.0_221 (Oracle Corporation 25.221-b11),
OS:           Mac OS X 10.15.2 x86_64

Jira : https://issues.apache.org/jira/browse/GROOVY-9353

2voto

Leonard Brünings Points 3017

Vous avez raison. @CompileStatic ne peut pas être utilisé en conjonction avec la manipulation de métaclasses. La raison en est que, comme son nom l'indique, tout est résolu et lié au moment de la compilation, il n'y a donc pas de recherche de métaclasses et il n'y a donc aucun moyen de passer outre.

Je vous suggère de vous pencher sur l'injection IoC/dépendance, afin de pouvoir injecter l'objet fantaisie dans votre code. Travailler avec des singletons classiques rend votre code beaucoup plus difficile à tester.

2voto

Shashank Agrawal Points 14533

Après le commentaire de Leonard Brünings :

Oui, @CompileStatic résoudra le constructeur de RESTClient dans votre FooClass au moment de la compilation afin qu'il n'utilise pas la métaclasse pour le verrouiller au moment de l'exécution. Si vous voulez voir à quoi cela ressemble, je vous suggère d'utiliser un décompilateur, par exemple ByteCode viewer et de regarder le bytecode généré.

Nous avons décompilé le code octet généré pour deux scénarios :

Avec @CompileStatic

public class FooClass implements GroovyObject {
    public FooClass() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    public String getDataFromProvider() {
        String url = "https://www.example.com";

        // Directly constructor is getting used
        RESTClient restClient = new RESTClient(url);

        HttpResponseDecorator response = (HttpResponseDecorator)ScriptBytecodeAdapter.asType(restClient.post(ScriptBytecodeAdapter.createMap(new Object[0])), HttpResponseDecorator.class);
        return (String)ShortTypeHandling.castToString(response);
    }
}

Sans @CompileStatic

public class FooClass implements GroovyObject {
    public FooClass() {
        CallSite[] var1 = $getCallSiteArray();
        super();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    public String getDataFromProvider() {
        CallSite[] var1 = $getCallSiteArray();
        String url = "https://www.example.com";

        // Here Groovy's metaprogramming is into play instead of directly calling constructor
        RESTClient restClient = (RESTClient)ScriptBytecodeAdapter.castToType(var1[0].callConstructor(RESTClient.class, url), RESTClient.class);

        HttpResponseDecorator response = (HttpResponseDecorator)ScriptBytecodeAdapter.asType(var1[1].call(restClient, ScriptBytecodeAdapter.createMap(new Object[0])), HttpResponseDecorator.class);
        return (String)ShortTypeHandling.castToString(response);
    }
}

La réponse donnée par @Leonard est donc tout à fait correcte. Nous avons manqué ce simple concept Java.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X