Stubbing inacabado detectado em Mockito

151

Estou recebendo a seguinte exceção durante a execução dos testes. Estou usando o Mockito para zombar. As dicas mencionadas pela biblioteca Mockito não estão ajudando.

org.mockito.exceptions.misusing.UnfinishedStubbingException: 
Unfinished stubbing detected here:
    -> at com.a.b.DomainTestFactory.myTest(DomainTestFactory.java:355)

    E.g. thenReturn() may be missing.
    Examples of correct stubbing:
        when(mock.isOk()).thenReturn(true);
        when(mock.isOk()).thenThrow(exception);
        doThrow(exception).when(mock).someVoidMethod();
    Hints:
     1. missing thenReturn()
     2. you are trying to stub a final method, you naughty developer!

        at a.b.DomainTestFactory.myTest(DomainTestFactory.java:276)
        ..........

Código de teste de DomainTestFactory. Quando executo o teste a seguir, vejo a exceção.

@Test
public myTest(){
    MyMainModel mainModel =  Mockito.mock(MyMainModel.class);
    Mockito.when(mainModel.getList()).thenReturn(getSomeList()); // Line 355
}

private List<SomeModel> getSomeList() {
    SomeModel model = Mockito.mock(SomeModel.class);
    Mockito.when(model.getName()).thenReturn("SomeName"); // Line 276
    Mockito.when(model.getAddress()).thenReturn("Address");
    return Arrays.asList(model);
}

public class SomeModel extends SomeInputModel{
    protected String address;
    protected List<SomeClass> properties;

    public SomeModel() {
        this.Properties = new java.util.ArrayList<SomeClass>(); 
    }

    public String getAddress() {
        return this.address;
    }

}

public class SomeInputModel{

    public NetworkInputModel() {
        this.Properties = new java.util.ArrayList<SomeClass>(); 
    }

    protected String Name;
    protected List<SomeClass> properties;

    public String getName() {
        return this.Name;
    }

    public void setName(String value) {
        this.Name = value;
    }
}
Royal Rose
fonte
Olá Mureinik, eu atualizei a postagem com números de linha
Royal Rose

Respostas:

371

Você está aninhando zombaria dentro de zombaria. Você está ligando getSomeList(), o que faz algumas zombarias, antes de terminar a zombaria MyMainModel. Mockito não gosta quando você faz isso.

Substituir

@Test
public myTest(){
    MyMainModel mainModel =  Mockito.mock(MyMainModel.class);
    Mockito.when(mainModel.getList()).thenReturn(getSomeList()); --> Line 355
}

com

@Test
public myTest(){
    MyMainModel mainModel =  Mockito.mock(MyMainModel.class);
    List<SomeModel> someModelList = getSomeList();
    Mockito.when(mainModel.getList()).thenReturn(someModelList);
}

Para entender por que isso causa um problema, você precisa saber um pouco sobre como o Mockito funciona e também estar ciente de que expressões e instruções de ordem são avaliadas em Java.

O Mockito não pode ler o seu código-fonte, portanto, para descobrir o que você está pedindo, ele depende muito do estado estático. Quando você chama um método em um objeto simulado, o Mockito registra os detalhes da chamada em uma lista interna de chamadas. O whenmétodo lê a última dessas invocações da lista e registra essa invocação no OngoingStubbingobjeto que retorna.

A linha

Mockito.when(mainModel.getList()).thenReturn(someModelList);

causa as seguintes interações com o Mockito:

  • O método simulado mainModel.getList()é chamado,
  • O método estático whené chamado,
  • O método thenReturné chamado no OngoingStubbingobjeto retornado pelo whenmétodo.

O thenReturnmétodo pode instruir a simulação que recebeu por meio do OngoingStubbingmétodo para manipular qualquer chamada adequada ao getListmétodo para retornar someModelList.

De fato, como o Mockito não pode ver seu código, você também pode escrever sua zombaria da seguinte maneira:

mainModel.getList();
Mockito.when((List<SomeModel>)null).thenReturn(someModelList);

Esse estilo é um pouco menos claro de ler, especialmente porque, nesse caso, nullele deve ser convertido, mas gera a mesma sequência de interações com o Mockito e alcançará o mesmo resultado da linha acima.

No entanto, a linha

Mockito.when(mainModel.getList()).thenReturn(getSomeList());

causa as seguintes interações com o Mockito:

  1. O método simulado mainModel.getList()é chamado,
  2. O método estático whené chamado,
  3. Um novo mockde SomeModelé criado (no interior getSomeList()),
  4. O método simulado model.getName()é chamado,

Nesse ponto, Mockito fica confuso. Ele pensou que você estava zombando mainModel.getList(), mas agora você está dizendo que deseja zombar do model.getName()método. Para Mockito, parece que você está fazendo o seguinte:

when(mainModel.getList());
// ...
when(model.getName()).thenReturn(...);

Isso parece bobagem, Mockitopois não pode ter certeza do que você está fazendo mainModel.getList().

Observe que não chegamos à thenReturnchamada do método, pois a JVM precisa avaliar os parâmetros desse método antes de poder chamá-lo. Nesse caso, isso significa chamar o getSomeList()método.

Geralmente, é uma má decisão de projeto confiar no estado estático, como Mockito, porque pode levar a casos em que o Princípio da menor surpresa é violado. No entanto, o design de Mockito resulta em zombarias claras e expressivas, mesmo que algumas vezes causem espanto.

Finalmente, as versões recentes do Mockito adicionam uma linha extra à mensagem de erro acima. Essa linha extra indica que você pode estar na mesma situação que esta pergunta:

3: você está reprimindo o comportamento de outro mock dentro da instrução 'thenReturn', se concluída

Luke Woodward
fonte
Existe alguma explicação para esse fato? Solução funciona. E não entendo por que a criação simulada 'no local' não funciona. Quando você cria um mock e passa o mock criado por referência a outro mock, ele funciona.
Capacytron
1
Excelente resposta, amor ASSIM! Ele teria me levado as idades para encontrar este por mim
Dici
4
Ótima resposta Luke! Explicação muito detalhada em palavras simples. Obrigado.
Tomasz Kalkosiński 30/10
1
Impressionante. O engraçado é que, quando eu faço a chamada direta do método e depuro lentamente, ele funciona. O atributo CGLIB $ BOUND obterá o valor verdadeiro, mas, de alguma forma, leva um pouco de tempo. Quando uso a chamada direta do método e paro antes do treinamento (quando ...), vejo que o valor é primeiro falso e depois se torna verdadeiro. Quando é falso e o treinamento é iniciado, essa exceção ocorre.
Michael Hegner
Você fez meu dia! Esse é o tipo de erro que faz você perder muito tempo! Eu pensei que era algo relacionado a Kotlin no início
Bronx
1
org.mockito.exceptions.misusing.UnfinishedStubbingException: 
Unfinished stubbing detected here:
E.g. thenReturn() may be missing.

Para zombar de métodos nulos, tente abaixo:

//Kotlin Syntax

 Mockito.`when`(voidMethodCall())
           .then {
                Unit //Do Nothing
            }
takharsh
fonte