Mockito: Como zombar apenas da chamada de um método da superclasse


94

Estou usando o Mockito em alguns testes.

Tenho as seguintes aulas:

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

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

Eu quero zombar apenas da segunda chamada ( super.save) de ChildService. A primeira chamada deve chamar o método real. Existe uma maneira de fazer isso?


Isso pode ser resolvido com PowerMockito?
javaPlease42

@ javaPlease42: Sim, você pode: stackoverflow.com/a/23884011/2049986 .
Jacob van Lingen

Respostas:


57

Não, o Mockito não suporta isso.

Pode não ser a resposta que você está procurando, mas o que você está vendo é um sintoma de não aplicação do princípio de design:

Favorecer a composição ao invés da herança

Se você extrair uma estratégia em vez de estender uma superclasse, o problema desaparecerá.

Se, no entanto, você não tem permissão para alterar o código, mas deve testá-lo de qualquer maneira, e dessa maneira estranha, ainda há esperança. Com algumas ferramentas AOP (por exemplo AspectJ), você pode entrelaçar o código no método da superclasse e evitar sua execução totalmente (eca). Isso não funciona se você estiver usando proxies, você deve usar a modificação de bytecode (ou entrelaçamento do tempo de carregamento ou entrelaçamento do tempo de compilação). Existem frameworks de mocking que suportam esse tipo de truque também, como PowerMock e PowerMockito.

Eu sugiro que você vá para a refatoração, mas se essa não for uma opção, você se divertirá seriamente hackeando.


5
Não estou vendo a violação do LSP. Eu tenho quase a mesma configuração que o OP: uma classe base DAO com um método findAll () e uma subclasse DAO que substitui o método base chamando super.findAll () e classificando o resultado. A subclasse é substituível em todos os contextos que aceitam a superclasse. Estou entendendo mal o seu significado?

1
Vou remover a observação LSP (não agrega valor à resposta).
iwein

Sim, herança é uma droga e a estrutura estúpida na qual estou preso é projetada com herança como a única opção.
Sridhar Sarnobat

Supondo que você não possa reprojetar a superclasse, você pode extrair o //some codescódigo em um método que pode ser testado separadamente.
Phasmal de

1
OK, entendi. É um problema diferente do que eu estava tentando resolver quando procurei por isso, mas pelo menos aquele mal-entendido resolveu meu próprio problema de simulação de chamada da classe base (que eu realmente não anulo).
Guillaume Perrot de

86

Se você realmente não tem escolha para refatorar, você pode simular / fazer stub de tudo na chamada do super método, por exemplo

    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();
    }

2
este código não impediria o direito de invocação super.save (), então se você fizer muito no super.save () você terá que evitar todas essas chamadas ...
iwein

solução fantástica faz maravilhas para mim quando desejo retornar um valor simulado de um método da superclasse para uso pela criança, fantástico, obrigado.
Bacamarte

14
isso funciona bem, a menos que o validate esteja oculto ou o método save faça o trabalho diretamente em vez de chamar outro método. mockito não: Mockito.doNothing (). when ((BaseService) spy) .save (); isso não fará nada no serviço de base, mas sim no childService :(
tibi

Não consigo fazer isso funcionar no meu - ele também elimina o método filho. BaseServiceé abstrato, embora eu não veja por que isso seria relevante.
Sridhar Sarnobat

1
@ Sridhar-Sarnobat sim, estou vendo a mesma coisa :( alguém sabe como fazer para apenas apagar o super.validate()?
stantonk

4

Considere refatorar o código do método ChildService.save () para um método diferente e teste esse novo método em vez de testar ChildService.save (), dessa forma você evitará chamadas desnecessárias para o método super.

Exemplo:

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

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

1

crie um método protegido por pacote (assume a classe de teste no mesmo pacote) na subclasse que chama o método da superclasse e, a seguir, chame esse método no método da subclasse substituído. você pode então definir expectativas sobre este método em seu teste através do uso do padrão espião. não bonito, mas certamente melhor do que ter que lidar com toda a configuração de expectativa para o super método em seu teste


posso dizer também que composição sobre herança é quase sempre melhor, mas às vezes é simplesmente mais simples de usar herança. até que o java incorpore um modelo de composição melhor, como scala ou groovy, esse sempre será o caso e esse problema continuará existindo
Lucas

1

Mesmo que eu concorde totalmente com a resposta do iwein (

favorecer a composição sobre a herança

), admito que às vezes a herança parece natural e não sinto quebrá-la ou refatorá-la apenas por causa de um teste de unidade.

Então, minha sugestão:

/**
 * 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();
    }  
}

E então, no teste de unidade:

    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

0

A razão é que sua classe base não é pública, então o Mockito não pode interceptá-la devido à visibilidade, se você alterar a classe base como pública, ou @Override na subclasse (como pública), então o Mockito pode simular isso corretamente.

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());
}

8
Esse não é o ponto. ChildService deve substituir foo () e o problema é como simular BaseService.foo (), mas não ChildService.foo ()
Adriaan Koster

0

Talvez a opção mais fácil se a herança fizer sentido é criar um novo método (pacote privado ??) para chamar o super (vamos chamá-lo de superFindall), espionar a instância real e, em seguida, simular o método superFindAll () da maneira que você deseja simular a classe pai um. Não é a solução perfeita em termos de cobertura e visibilidade, mas deve dar conta do recado e é fácil de aplicar.

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

    void superSave(){
        super.save();
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.