Mockito Cómo burlarse solo de la llamada de un método de la superclase

94

Estoy usando Mockito en algunas pruebas.

Tengo las siguientes clases:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        //some code  
        super.save();
    }  
}   

Quiero burlarme solo de la segunda llamada ( super.save) de ChildService. La primera llamada debe llamar al método real. ¿Hay una manera de hacer eso?

mada
fuente
¿Se puede solucionar esto con PowerMockito?
javaPlease42
@ javaPlease42: Sí, puede hacerlo: stackoverflow.com/a/23884011/2049986 .
Jacob van Lingen

Respuestas:

57

No, Mockito no admite esto.

Puede que esta no sea la respuesta que está buscando, pero lo que está viendo es un síntoma de no aplicar el principio de diseño:

Favorecer la composición sobre la herencia

Si extrae una estrategia en lugar de extender una superclase, el problema desaparece.

Sin embargo, si no se le permite cambiar el código, pero debe probarlo de todos modos, y de esta manera incómoda, todavía hay esperanza. Con algunas herramientas AOP (por ejemplo, AspectJ) puedes tejer código en el método de superclase y evitar su ejecución por completo (puaj). Esto no funciona si está usando proxies, debe usar la modificación del código de bytes (ya sea tejido en tiempo de carga o tejido en tiempo de compilación). Hay marcos de burla que también admiten este tipo de trucos, como PowerMock y PowerMockito.

Te sugiero que optes por la refactorización, pero si esa no es una opción, te espera una gran diversión de piratería.

iwein
fuente
5
No veo la violación de LSP. Tengo aproximadamente la misma configuración que el OP: una clase DAO base con un método findAll () y una subclase DAO que anula el método base llamando a super.findAll () y luego ordenando el resultado. La subclase es sustituible en todos los contextos que aceptan la superclase. ¿Estoy entendiendo mal tu significado?
1
Eliminaré el comentario de LSP (no agrega valor a la respuesta).
iwein
Sí, la herencia apesta y el estúpido marco con el que estoy atrapado está diseñado con la herencia como única opción.
Sridhar Sarnobat
Suponiendo que no puede rediseñar la superclase, podría extraer el //some codescódigo en un método que podría probarse por separado.
Phasmal
1
Ok lo tengo. Es un problema diferente al que estaba tratando de resolver cuando busqué esto, pero al menos ese malentendido resolvió mi propio problema para simular una llamada desde la clase base (que no anulo de hecho).
Guillaume Perrot
86

Si realmente no tiene una opción para refactorizar, puede simular / eliminar todo en la llamada al súper método, por ejemplo

    class BaseService {

        public void validate(){
            fail(" I must not be called");
        }

        public void save(){
            //Save method of super will still be called.
            validate();
        }
    }

    class ChildService extends BaseService{

        public void load(){}

        public void save(){
            super.save();
            load();
        }
    }

    @Test
    public void testSave() {
        ChildService classToTest = Mockito.spy(new ChildService());

        // Prevent/stub logic in super.save()
        Mockito.doNothing().when((BaseService)classToTest).validate();

        // When
        classToTest.save();

        // Then
        verify(classToTest).load();
    }
jgonian
fuente
2
este código en realidad no evitaría el derecho de invocación de super.save (), por lo que si hace mucho en super.save () tendría que evitar todas esas llamadas ...
iwein
La solución fantástica funciona de maravilla para mí cuando quiero devolver un valor simulado de un método de superclase para que lo use el niño, fantástico gracias.
Rubio
14
esto funciona bien a menos que la validación esté oculta o el método de guardado haga el trabajo directamente en lugar de llamar a otro método. mockito no: Mockito.doNothing (). when ((BaseService) espía) .save (); esto no guardará nada en el servicio base, sino en el servicio childService :(
tibi
No puedo hacer que esto funcione en el mío, también elimina el método del niño. BaseServicees abstracto, aunque no veo por qué eso sería relevante.
Sridhar Sarnobat
1
@ Sridhar-Sarnobat sí, estoy viendo lo mismo :( ¿alguien sabe cómo hacer que solo super.validate()
elimine
4

Considere refactorizar el código del método ChildService.save () a un método diferente y pruebe ese nuevo método en lugar de probar ChildService.save (), de esta manera evitará llamadas innecesarias al súper método.

Ejemplo:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        newMethod();    
        super.save();
    }
    public void newMethod(){
       //some codes
    }
} 
Mohammed Misbahuddin
fuente
1

cree un método protegido por paquete (asume la clase de prueba en el mismo paquete) en la subclase que llama al método de superclase y luego llame a ese método en su método de subclase anulado. Luego, puede establecer expectativas sobre este método en su prueba mediante el uso del patrón de espionaje. no es bonito, pero ciertamente es mejor que tener que lidiar con todas las expectativas establecidas para el súper método en su prueba

Luke
fuente
También puedo decir que la composición sobre la herencia es casi siempre mejor, pero a veces es simplemente más simple usar la herencia. hasta que java incorpore un mejor modelo de composición, como scala o groovy, este será siempre el caso y este problema seguirá existiendo
Lucas
1

Incluso si estoy totalmente de acuerdo con la respuesta de iwein (

favorecer la composición sobre la herencia

), admito que hay ocasiones en las que la herencia parece natural, y no siento que se rompa o refactorice solo por el bien de una prueba unitaria.

Entonces, mi sugerencia:

/**
 * BaseService is now an asbtract class encapsulating 
 * some common logic callable by child implementations
 */
abstract class BaseService {  
    protected void commonSave() {
        // Put your common work here
    }

    abstract void save();
}

public ChildService extends BaseService {  
    public void save() {
        // Put your child specific work here
        // ...

        this.commonSave();
    }  
}

Y luego, en la prueba unitaria:

    ChildService childSrv = Mockito.mock(ChildService.class, Mockito.CALLS_REAL_METHODS);

    Mockito.doAnswer(new Answer<Void>() {
        @Override
        public Boolean answer(InvocationOnMock invocation)
                throws Throwable {
            // Put your mocked behavior of BaseService.commonSave() here
            return null;
        }
    }).when(childSrv).commonSave();

    childSrv.save();

    Mockito.verify(childSrv, Mockito.times(1)).commonSave();

    // Put any other assertions to check child specific work is done
represas50
fuente
0

La razón es que su clase base no es public-ed, entonces Mockito no puede interceptarla debido a la visibilidad, si cambia la clase base como pública, o @Override en subclase (como pública), entonces Mockito puede burlarse de ella correctamente.

public class BaseService{
  public boolean foo(){
    return true;
  }
}

public ChildService extends BaseService{
}

@Test
@Mock ChildService childService;
public void testSave() {
  Mockito.when(childService.foo()).thenReturn(false);

  // When
  assertFalse(childService.foo());
}
zoufeiyy
fuente
8
Este no es el punto. ChildService debería anular foo () y el problema es cómo simular BaseService.foo () pero no ChildService.foo ()
Adriaan Koster
0

Tal vez la opción más fácil si la herencia tiene sentido es crear un nuevo método (paquete privado?) Para llamar al super (llamémoslo superFindall), espiar la instancia real y luego burlarse del método superFindAll () de la manera que deseaba simular la clase principal uno. No es la solución perfecta en términos de cobertura y visibilidad, pero debería funcionar y es fácil de aplicar.

 public Childservice extends BaseService {
    public void save(){
        //some code
        superSave();
    }

    void superSave(){
        super.save();
    }
}
Rubasace
fuente