Burlándose de las variables miembro de una clase usando Mockito

136

Soy un novato en desarrollo y en pruebas unitarias en particular. Supongo que mi requisito es bastante simple, pero estoy ansioso por saber qué piensan los demás sobre esto.

Supongamos que tengo dos clases así:

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 estoy escribiendo prueba unitaria para probar el First.doSecond()método. Sin embargo, supongamos que quiero burlarme de la Second.doSecond()clase así. Estoy usando Mockito para hacer esto.

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

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

Estoy viendo que la burla no tiene efecto y la afirmación falla. ¿No hay forma de burlarse de las variables miembro de una clase que quiero probar? ?

Anand Hemmige
fuente

Respuestas:

86

Debe proporcionar una forma de acceder a las variables miembro para poder pasar de manera simulada (las formas más comunes serían un método de establecimiento o un constructor que toma un parámetro).

Si su código no proporciona una forma de hacerlo, está incorrectamente factorizado para TDD (Test Driven Development).

gatito
fuente
44
Gracias. Yo lo veo. Me pregunto cómo puedo realizar pruebas de integración usando simulacros donde puede haber muchos métodos internos, clases que pueden necesitar simularse, pero no necesariamente disponibles para configurarse a través de un setXXX () de antemano.
Anand Hemmige
2
Use un marco de inyección de dependencia, con una configuración de prueba. Dibuje un diagrama de secuencia de la prueba de integración que está intentando realizar. Factoriza el diagrama de secuencia en los objetos que realmente puedes controlar. Esto significa que si está trabajando con una clase de marco que tiene el antipatrón de objeto dependiente que muestra arriba, entonces debe considerar el objeto y su miembro mal factorizado como una sola unidad en términos del diagrama de secuencia. Esté preparado para ajustar la factorización de cualquier código que controle, para que sea más comprobable.
kittylyst
9
Estimado @kittylyst, sí, probablemente sea incorrecto desde el punto de vista TDD o desde cualquier punto de vista racional. Pero a veces un desarrollador trabaja en lugares donde nada tiene sentido y el único objetivo que tiene es completar las historias que ha asignado y desaparecer. Sí, está mal, no tiene sentido, las personas no calificadas toman las decisiones clave y todas esas cosas. Entonces, al final del día, los antipatrones ganan por mucho.
amanas
1
Tengo curiosidad acerca de esto, si un miembro de la clase no tiene ninguna razón para establecerse desde el exterior, ¿por qué deberíamos crear un setter solo con el propósito de probarlo? Imagine que la clase 'Segunda' aquí es en realidad un administrador o herramienta FileSystem, inicializada durante la construcción del objeto a probar. Tengo todas las razones para querer burlarme de este administrador de FileSystem, para probar la primera clase, y cero razones para hacerlo accesible. Puedo hacer esto en Python, entonces ¿por qué no con Mockito?
Zangdar
65

Esto no es posible si no puede cambiar su código. Pero me gusta la inyección de dependencia y Mockito lo admite:

public class First {    
    @Resource
    Second second;

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

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

Su prueba:

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

Esto es muy lindo y fácil.

Janning
fuente
2
Creo que esta es una mejor respuesta que las otras porque InjectMocks.
sudocoder
Es curioso cómo uno, como novato de pruebas como yo, confía en ciertas bibliotecas y marcos. Asumí que esto era solo una mala idea que indicaba la necesidad de rediseñar ... hasta que me demostró que es posible (de manera muy clara y limpia) en Mockito.
Mike roedor
9
¿Qué es @Resource ?
IgorGanapolsky
3
@IgorGanapolsky el @ Resource es una anotación creada / utilizada por el marco Java Spring. Es una forma de indicarle a Spring que se trata de un bean / objeto administrado por Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire No es una cosa falsa, pero debido a que se usa en la clase sin prueba, debe ser burlado en el prueba.
Grez.Kev
No entiendo esta respuesta. ¿Dices que no es posible y luego muestras que es posible? ¿Qué es exactamente lo que no es posible aquí?
Goldname
35

Si observa detenidamente su código, verá que la secondpropiedad en su prueba sigue siendo una instancia Second, no un simulacro (no pasa el simulacro firsten su código).

La forma más sencilla sería crear un setter seconden Firstclase y pasarle el simulacro explícitamente.

Me gusta esto:

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

Otra sería pasar una Secondinstancia como Firstparámetro constructor.

Si no puede modificar el código, creo que la única opción sería usar la reflexión:

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

Pero probablemente pueda, ya que es raro hacer pruebas en el código que no controla (aunque uno puede imaginar un escenario en el que tiene que probar una biblioteca externa porque su autor no lo hizo :))

verificación del alma
fuente
Entendido. Probablemente iré con tu primera sugerencia.
Anand Hemmige
Curiosamente, ¿hay alguna forma o API que conozca que pueda burlarse de un objeto / método a nivel de aplicación o paquete? ? Supongo que lo que estoy diciendo es que, en el ejemplo anterior, cuando me burlo del objeto 'Segundo', ¿hay alguna forma de que pueda anular cada instancia de Segundo que se usa durante el ciclo de vida de las pruebas? ?
Anand Hemmige
@AnandHemmige en realidad el segundo (constructor) es más limpio, ya que evita crear instancias innecesarias de 'segundos'. Sus clases se desacoplan muy bien de esa manera.
soulcheck
10
Mockito proporciona algunas anotaciones agradables que le permiten inyectar sus simulacros en variables privadas. Anotar Segundo con @Mocky anotar Primero con @InjectMockse instanciar Primero en el inicializador. Mockito automáticamente hará lo mejor para encontrar un lugar para inyectar el segundo simulacro en la primera instancia, incluida la configuración de campos privados que coincidan con el tipo.
jhericks
@Mockfue alrededor de 1.5 (tal vez antes, no estoy seguro). 1.8.3 introducido @InjectMocks, así como @Spyy @Captor.
jhericks
7

Si no puede cambiar la variable miembro, al revés, use powerMockit y llame

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

Ahora el problema es que CUALQUIER llamada al nuevo Segundo devolverá la misma instancia simulada. Pero en su caso simple esto funcionará.

usuario1509463
fuente
6

Tuve el mismo problema donde no se estableció un valor privado porque Mockito no llama a superconstructores. Así es como aumento la burla con la reflexión.

Primero, creé una clase TestUtils que contiene muchas utilidades útiles, incluidos estos métodos de reflexión. El acceso a la reflexión es un poco difícil de implementar cada vez. Creé estos métodos para probar el código en proyectos que, por una razón u otra, no tenían un paquete de simulación y no me invitaron a incluirlo.

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

}

Entonces puedo probar la clase con una variable privada como esta. Esto es útil para burlarse de los árboles profundos de la clase que tampoco tienes control.

@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
}

Modifiqué mi código de mi proyecto real aquí, en la página. Podría haber un problema de compilación o dos. Creo que entiendes la idea general. Siéntase libre de tomar el código y usarlo si lo encuentra útil.

Dave
fuente
¿Podría explicar su código con un caso de uso real? como Public class tobeMocker () {private ClassObject classObject; } Donde classObject es igual al objeto a ser reemplazado.
Jasper Lankhorst el
En su ejemplo, si ToBeMocker instance = new ToBeMocker (); y ClassObject someNewInstance = new ClassObject () {@Override // algo así como una dependencia externa}; luego TestUtils.refelctSetValue (instancia, "classObject", someNewInstance); Tenga en cuenta que tiene que averiguar qué desea anular para burlarse. Digamos que tiene una base de datos y esta anulación devolverá un valor, por lo que no necesita seleccionar. Más recientemente tuve un bus de servicio que no quería procesar realmente el mensaje pero quería asegurarme de que lo recibiera. Por lo tanto, configuré la instancia de bus privado de esta manera: ¿Útil?
Dave
Tendrás que imaginar que hubo formato en ese comentario. Fue removido. Además, esto no funcionará con Java 9 ya que bloqueará el acceso privado. Tendremos que trabajar con algunas otras construcciones una vez que tengamos un lanzamiento oficial y podamos trabajar dentro de sus límites reales.
Dave
1

Muchos otros ya te han aconsejado que reconsideres tu código para hacerlo más comprobable, un buen consejo y, por lo general, más simple de lo que voy a sugerir.

Si no puede cambiar el código para hacerlo más comprobable, PowerMock: https://code.google.com/p/powermock/

PowerMock extiende Mockito (para que no tenga que aprender un nuevo marco simulado), proporcionando funcionalidad adicional. Esto incluye la posibilidad de que un constructor devuelva un simulacro. Potente, pero un poco complicado, así que úsalo juiciosamente.

Utiliza un corredor simulado diferente. Y debe preparar la clase que va a invocar al constructor. (Tenga en cuenta que este es un problema común: prepare la clase que llama al constructor, no la clase construida)

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

Luego, en su configuración de prueba, puede usar el método whenNew para que el constructor devuelva un simulacro

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

Sí, esto se puede hacer, como muestra la siguiente prueba (escrito con la API de burla de JMockit, que desarrollo):

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

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

Con Mockito, sin embargo, tal prueba no se puede escribir. Esto se debe a la forma en que se implementa la burla en Mockito, donde se crea una subclase de la clase a burlarse; solo las instancias de esta subclase "simulada" pueden tener un comportamiento simulado, por lo que debe hacer que el código probado las use en lugar de cualquier otra instancia.

Rogério
fuente
3
la pregunta no era si JMockit es mejor o no que Mockito, sino cómo hacerlo en Mockito. ¡Apégate a construir un mejor producto en lugar de buscar la oportunidad de destruir la competencia!
TheZuck
8
El póster original solo dice que está usando Mockito; solo está implícito que Mockito es un requisito fijo y difícil, por lo que la pista de que JMockit puede manejar esta situación no es tan inapropiada.
Bombe
0

Si desea una alternativa a ReflectionTestUtils de Spring en mockito, use

Whitebox.setInternalState(first, "second", sec);
Szymon Zwoliński
fuente
¡Bienvenido a Stack Overflow! Hay otras respuestas que proporcionan la pregunta del OP, y se publicaron hace muchos años. Cuando publique una respuesta, asegúrese de agregar una nueva solución o una explicación sustancialmente mejor, especialmente al responder preguntas anteriores o comentar otras respuestas.
help-info.de