Estoy tratando de preparar una configuración abstracta para un objeto probado y estoy atascado cuando se trata de usar un Stub allí. Básicamente, lo que estoy tratando de lograr: tengo una fachada que se ve así:

@AllArgsConstructor
public class Facade {

    private final EventBus events;

    Mono<String> doSomething() {
        return just("someId").doOnNext(id -> events.push(new ExpectedEvent(id)));
    }
    
    
    /** And many other methods required to initialize the facade in the mentioned test **/
}

Con EventBus como:

public interface EventBus {
    void push(Event event);
}

Y evento de muestra:

@Value
public class ExpectedEvent implements Event {
    String id;
}

Tengo diferentes clases de prueba para diferentes casos de uso; en la mayoría de las pruebas no necesito interactuar con eventBus, por lo que me gustaría que fuera una implementación simple como

event -> just("No event bus configured")

Pero también me gustaría tener la posibilidad de verificar si los eventos adecuados se publicaron allí, por lo que también me gustaría poder inyectar un Stub si es necesario. Otro aspecto es que necesito algo de código en el método setupSpec para configurar correctamente la fachada antes de las pruebas y me gustaría evitar hacerlo en el método de configuración.

Cómo lo vería yo:

abstract class AbstractSpec extends Specification {

@Shared
Facade facade = new Facade(
        eventBus())

def setupSpec() {
    /** run different methods on the facade to prepare it for the tests **/
}

EventBus eventBus() {
    return { event -> just("No event bus configured") }
}

Entonces todas las pruebas "regulares" simplemente heredarían de AbstractSpec y simplemente llamarían a la fachada inicializada. Mientras que en la clase donde me gustaría verificar las llamadas de EventBus, tendría algo como esto:

class DerivedSpec extends AbstractSpec {

EventBus eventBus = Mock()

def "check if proper event was emited"() {
    given:
        ExpectedEvent publishedEvent

    when:
        def someId = facade.doSomething().block()

    then:
        1 * eventBus.push(_ as ExpectedEvent) >> { ExpectedEvent event -> publishedEvent = event }
        publishedEvent.id() == someId
}

EventBus eventBus() {
    return eventBus
}
}

¿Es alcanzable de alguna manera? El código anterior tiene este problema de que no puedo usar Mock con el objeto @Shared. Me gustaría tener una inicialización común de la fachada en la mayoría de las pruebas, pero anular la inicialización en algunas, usando una simulación de EventBus para verificar las interacciones con ella.

¿Son útiles las extensiones de Spock en tal caso? Supongo que la solución más simple sería renunciar a extender AbstractSpec en esta prueba en particular y simplemente reutilizar el código setupSpec en la configuración para esta única clase, pero tengo curiosidad por saber si hay otra forma de resolverlo.

0
Maciekkk 30 ago. 2020 a las 00:51

1 respuesta

La mejor respuesta

No estoy realmente seguro de si su tipo de configuración es la forma ideal de resolver esto, parece bastante artificial y difícil de entender y mantener. Probablemente elegiría un diseño de prueba diferente. De todos modos, quiero limitarme a responder estrictamente a su pregunta aplicando cambios mínimos a su especificación.

Aquí tienes un problema de arranque: en AbstractSpec usas un miembro @Shared, que en muchos casos ya es un patrón anti porque significa que hay una variable que lleva el estado entre los métodos de características, es decir, tus pruebas podría ser sensible a la orden de ejecución, lo cual es malo. Suponiendo que tiene varias subclases para esta clase principal abstracta, incluso podría haber efectos secundarios entre diferentes especificaciones de prueba (clases), lo cual es aún peor. Pero sea como sea, déjame tomar tu variable compartida como un hecho.

Ahora, esta variable compartida es casi como una variable estática, es decir, Spock la inicializará antes que cualquier variable de instancia normal. Por lo tanto, tratar de asignarle un valor llamando a un método anulado por una subclase y ese método tratando de asignar un valor que solo se inicializará más tarde hace que la variable compartida obtenga el valor null, que luego causa un {{X1 }} al intentar llamar a métodos sobre el objeto.

Entonces, lo que debe hacer es crear el objeto simulado en la subclase static. Pero entonces tienes el siguiente problema: las llamadas de Spock Mock() solo funcionan en un contexto no estático. Por lo tanto, está teniendo el próximo problema de la gallina contra el huevo aquí. La forma de solucionarlo es crear un simulacro independiente y luego adjúntelo manualmente a la instancia de especificación durante la ejecución de la prueba (@AutoAttach no funciona en este caso). Los simulacros separados se introdujeron principalmente para ser utilizados por marcos DI como Spring o Guice, pero también se pueden usar de forma independiente.

Para mí, su prueba se ejecuta así:

package de.scrum_master.stackoverflow.q63652119

import org.spockframework.mock.MockUtil
import spock.mock.DetachedMockFactory

class DerivedSpec extends AbstractSpec {
  static mockFactory = new DetachedMockFactory()
  static EventBus eventBus = mockFactory.Mock(EventBus)

  def "check if proper event was emited"() {
    given:
    ExpectedEvent publishedEvent
    new MockUtil().attachMock(eventBus, this)

    when:
    def someId = facade.doSomething().block()

    then:
    1 * eventBus.push(_ as ExpectedEvent) >> { ExpectedEvent event -> publishedEvent = event }
    publishedEvent.id == someId
  }

  EventBus eventBus() {
    return eventBus
  }
}
1
kriegaex 31 ago. 2020 a las 02:48