Zombando de variáveis ​​de membro de uma classe usando o Mockito

136

Sou novato no desenvolvimento e nos testes de unidade em particular. Eu acho que meu requisito é bastante simples, mas estou interessado em saber o que os outros pensam sobre isso.

Suponha que eu tenha duas classes assim -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Digamos que estou escrevendo teste de unidade para testar o First.doSecond()método. No entanto, suponha, eu quero a Second.doSecond()classe Mock assim. Estou usando o Mockito para fazer isso.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Estou vendo que a zombaria não entra em vigor e a afirmação falha. Não há como zombar das variáveis ​​de membro de uma classe que eu quero testar. ?

Anand Hemmige
fonte

Respostas:

86

Você precisa fornecer uma maneira de acessar as variáveis ​​de membro para poder passar de forma fictícia (as formas mais comuns seriam um método setter ou um construtor que aceita um parâmetro).

Se o seu código não fornecer uma maneira de fazer isso, ele será fatorado incorretamente no TDD (Test Driven Development).

kittylyst
fonte
4
Obrigado. Eu vejo isso. Estou apenas imaginando como posso executar testes de integração usando o mock, onde pode haver muitos métodos internos, classes que talvez precisem ser ridicularizadas, mas não necessariamente disponíveis para serem definidas através de um setXXX () antes.
Anand Hemmige
2
Use uma estrutura de injeção de dependência, com uma configuração de teste. Faça um diagrama de sequência do teste de integração que você está tentando fazer. Fatore o diagrama de sequência nos objetos que você pode realmente controlar. Isso significa que, se você estiver trabalhando com uma classe de estrutura que possui o antipadrão dependente de objeto que você mostra acima, deverá considerar o objeto e seu membro mal fatorado como uma única unidade em termos do diagrama de sequência. Esteja preparado para ajustar o fatoramento de qualquer código que você controla, para torná-lo mais testável.
precisa saber é o seguinte
9
Caro @kittylyst, sim, provavelmente está errado do ponto de vista do TDD ou de qualquer tipo de ponto de vista racional. Mas, às vezes, um desenvolvedor trabalha em lugares onde nada faz sentido, e o único objetivo que se tem é apenas completar as histórias que você atribuiu e desaparecer. Sim, está errado, não faz sentido, pessoas não qualificadas tomam as principais decisões e tudo mais. Assim, no final do dia, os antipadrões ganham muito.
Amanas
1
Estou curioso sobre isso, se um aluno não tem motivos para ser definido a partir do exterior, por que devemos criar um levantador apenas com a finalidade de testá-lo? Imagine que a classe 'Second' aqui é realmente um gerenciador ou ferramenta do FileSystem, inicializado durante a construção do objeto a ser testado. Eu tenho todos os motivos para querer zombar desse gerenciador do FileSystem, para testar a Primeira classe e zero motivo para torná-lo acessível. Eu posso fazer isso em Python, então por que não com o Mockito?
Zangdar 21/04
65

Isso não é possível se você não puder alterar seu código. Mas eu gosto de injeção de dependência e o Mockito suporta:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Seu teste:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

Isso é muito agradável e fácil.

Janning
fonte
2
Eu acho que essa é uma resposta melhor do que as outras porque o InjectMocks.
sudocoder
É engraçado como alguém consegue, como novato em teste como eu, confiar em certas bibliotecas e estruturas. Eu estava assumindo que isso era apenas uma má idéia, indicando a necessidade de redesenhar ... até que você me mostrou que é realmente possível (muito clara e limpa) em Mockito.
mike roedor
9
O que é o @Resource ?
IgorGanapolsky
3
@IgorGanapolsky the @ Resource é uma anotação criada / usada pela estrutura Java Spring. É uma maneira de indicar ao Spring que este é um bean / objeto gerenciado pelo Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Não é uma coisa de mockito, mas como é usada na classe de não teste, ela deve ser zombada no diretório teste.
Grez.Kev
Eu não entendo essa resposta. Você diz que não é possível, então mostra que é possível? O que exatamente não é possível aqui?
Goldname 17/06
35

Se você olhar atentamente para o seu código, verá que a secondpropriedade em seu teste ainda é uma instância Second, não uma simulação (você não passa a simulação firstno seu código).

A maneira mais simples seria criar um setter para seconda Firstclasse e passá-lo o mock explicitamente.

Como isso:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Outra seria passar uma Secondinstância como Firstparâmetro construtor.

Se você não pode modificar o código, acho que a única opção seria usar a reflexão:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

Mas você provavelmente pode, pois é raro fazer testes no código que você não controla (embora se possa imaginar um cenário em que você precise testar uma biblioteca externa porque o autor não fez :))

verificação da alma
fonte
Entendi. Provavelmente irei com sua primeira sugestão.
Anand Hemmige
Apenas curioso, existe alguma maneira ou API de que você possa zombar de um objeto / método no nível do aplicativo ou no pacote. ? Acho que o que estou dizendo é que, no exemplo acima, quando eu zombo do objeto 'Second', existe uma maneira de substituir todas as instâncias do Second que são usadas durante o ciclo de vida dos testes. ?
Anand Hemmige
@AnandHemmige, na verdade, o segundo (construtor) é mais limpo, pois evita a criação de instâncias desnecessárias do `Second '. Suas aulas são bem dissociadas dessa maneira.
soulcheck
10
O Mockito fornece algumas anotações legais para permitir que você injete suas simulações em variáveis ​​privadas. Anote o segundo com @Mocke anote primeiro com @InjectMockse instancia primeiro no inicializador. O Mockito fará automaticamente o melhor para encontrar um local para injetar o segundo mock na primeira instância, incluindo a configuração de campos particulares que correspondem ao tipo.
jhericks
@Mockfoi em torno de 1,5 (talvez mais cedo, não tenho certeza). 1.8.3 introduzido @InjectMocks, bem como @Spye @Captor.
jhericks
7

Se você não pode alterar a variável de membro, o contrário é usar o powerMockit e chamar

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Agora, o problema é que QUALQUER chamada para o novo Second retornará a mesma instância simulada. Mas no seu caso simples, isso funcionará.

user1509463
fonte
6

Eu tive o mesmo problema em que um valor privado não foi definido porque o Mockito não chama super construtores. Aqui está como eu aumento a zombaria com a reflexão.

Primeiro, criei uma classe TestUtils que contém muitos utilitários úteis, incluindo esses métodos de reflexão. O acesso à reflexão é um pouco complicado de implementar a cada vez. Criei esses métodos para testar o código em projetos que, por um motivo ou outro, não tinham pacote de simulação e não fui convidado a incluí-lo.

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

Então eu posso testar a classe com uma variável privada como esta. Isso é útil para zombar das árvores de classe que você não tem controle também.

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

Modifiquei meu código do meu projeto atual aqui, na página. Pode haver um problema de compilação ou dois. Eu acho que você entendeu a ideia geral. Sinta-se livre para pegar o código e usá-lo, se achar útil.

Dave
fonte
Você poderia explicar seu código com um caso de usuário real? como Public class tobeMocker () {private ClassObject classObject; } Onde classObject é igual ao objeto a ser substituído.
Jasper Lankhorst 23/03
No seu exemplo, se ToBeMocker instance = new ToBeMocker (); e ClassObject someNewInstance = new ClassObject () {@Override // algo como uma dependência externa}; TestUtils.refelctSetValue (instância, "classObject", someNewInstance); Observe que você precisa descobrir o que deseja substituir por zombaria. Digamos que você tenha um banco de dados e essa substituição retornará um valor para que você não precise selecionar. Mais recentemente, eu tinha um barramento de serviço que não queria realmente processar a mensagem, mas queria garantir que ela a recebesse. Assim, defino a instância do barramento privado dessa maneira - Útil?
dave
Você terá que imaginar que havia formatação nesse comentário. Foi removido. Além disso, isso não funcionará com o Java 9, pois bloqueará o acesso privado. Teremos que trabalhar com outras construções quando tivermos um lançamento oficial e pudermos trabalhar dentro dos limites reais.
dave
1

Muitas pessoas já o aconselharam a repensar seu código para torná-lo mais testável - bons conselhos e geralmente mais simples do que o que estou prestes a sugerir.

Se você não pode alterar o código para torná-lo mais testável, PowerMock: https://code.google.com/p/powermock/

O PowerMock estende o Mockito (para que você não precise aprender uma nova estrutura simulada), fornecendo funcionalidade adicional. Isso inclui a capacidade de um construtor retornar um mock. Poderoso, mas um pouco complicado - portanto, use-o criteriosamente.

Você usa um corredor de simulação diferente. E você precisa preparar a classe que irá invocar o construtor. (Observe que essa é uma pegadinha comum - prepare a classe que chama o construtor, não a classe construída)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

Em sua configuração de teste, você pode usar o método whenNew para que o construtor retorne uma simulação

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
jwepurchase
fonte
0

Sim, isso pode ser feito, como mostra o seguinte teste (escrito com a API de simulação do JMockit, que eu desenvolvo):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Com o Mockito, no entanto, esse teste não pode ser gravado. Isso ocorre devido à maneira como o mocking é implementado no Mockito, onde é criada uma subclasse da classe a ser mocked; somente instâncias dessa subclasse "mock" podem ter comportamento simulado, portanto, é necessário que o código testado as use em vez de qualquer outra instância.

Rogério
fonte
3
a questão não era se o JMockit é melhor que o Mockito, mas como fazê-lo no Mockito. Atenha-se à construção de um produto melhor, em vez de procurar oportunidades para acabar com a concorrência!
TheZuck
8
O pôster original diz apenas que ele está usando Mockito; está implícito apenas que o Mockito é um requisito fixo e difícil, portanto a dica de que o JMockit pode lidar com essa situação não é inadequada.
Bombe
0

Se você deseja uma alternativa ao ReflectionTestUtils da Spring no mockito, use

Whitebox.setInternalState(first, "second", sec);
Szymon Zwoliński
fonte
Bem-vindo ao Stack Overflow! Existem outras respostas que fornecem a pergunta do OP e foram publicadas há muitos anos. Ao postar uma resposta, adicione uma nova solução ou uma explicação substancialmente melhor, especialmente ao responder a perguntas mais antigas ou comentar outras respostas.
Help 19-19