2012-01-24 28 views
91

Soy un novato en el desarrollo y pruebas unitarias en particular. Supongo que mi requisito es bastante simple, pero estoy interesado en conocer otras opiniones sobre esto.Variables de miembro burlón de una clase usando Mockito

Supongamos que tengo dos clases como 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 unidad de prueba para probar First.doSecond() método. Sin embargo, supongamos, quiero simular clase Second.doSecond() como tal. 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? ?

Respuesta

58

Debe proporcionar una forma de acceder a las variables miembro para que pueda pasar una simulación (las formas más comunes) sería un método setter o un constructor que toma un parámetro).

Si su código no proporciona una forma de hacerlo, se tiene en cuenta incorrectamente para TDD (Desarrollo controlado por prueba).

+1

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 ser burladas, pero no necesariamente disponibles para establecerse a través de un setXXX() de antemano. –

+2

Utilice un marco de inyección de dependencia, con una configuración de prueba. Dibuja un diagrama de secuencia de la prueba de integración que estás tratando de hacer. Factoriza el diagrama de secuencia en los objetos que realmente puedes controlar. Esto significa que si está trabajando con una clase de infraestructura que tiene el antiporno 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 el factoring de cualquier código que controle, para hacerlo más comprobable. – kittylyst

+4

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 simplemente 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 mucho. – amanas

30

Si te fijas bien en su código verá que la propiedad second en su prueba sigue siendo una instancia de Second, no una maqueta (que no pasa el simulacro de first en su código).

La manera más simple sería crear un setter para second en la clase First y pasarle el simulacro explícitamente.

De esta manera:

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 instancia Second como parámetro constructor First 's.

Si no puede modificar el código, creo que la única opción sería la de utilizar 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 es probable que pueda, ya que es raro que hacer pruebas en código que no controla (aunque uno puede imaginarse un escenario en el que tiene que probar una biblioteca externa porque su autor no :))

+0

Lo tengo. Probablemente vaya con tu primera sugerencia. –

+0

Simplemente curioso, ¿hay alguna forma o API de la que tenga conocimiento que pueda simular 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 todas las instancias de Segundo que se utilicen durante el ciclo de vida de las pruebas? ? –

+0

@AnandHemmige en realidad el segundo (constructor) es más limpio, ya que evita la creación innecesaria de "Segundas" instancias. Tus clases se desacoplan muy bien de esa manera. – soulcheck

42

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 agradable y fácil.

+0

Creo que esta es una mejor respuesta que las otras porque InjectMocks. – sudocoder

+0

Es curioso cómo uno, como novato en pruebas como yo, confía en ciertas bibliotecas y marcos. Asumía que esto era solo una mala idea que indicaba la necesidad de rediseñar ... hasta que me demostraste que ** es ** de hecho posible (muy claro y limpio) en Mockito. –

+3

¿Qué es ** @ Resource **? –

-1

Sí, esto se puede hacer, como muestra la siguiente prueba (escritas con la API JMockit burla, 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, una prueba de este tipo 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 burlado, por lo que debe hacer que el código probado las use en lugar de cualquier otra instancia.

+3

la pregunta no era si JMockit es mejor que Mockito o no, sino cómo hacerlo en Mockito. ¡Quédese con la construcción de un producto mejor en lugar de buscar la oportunidad de destrozar la competencia! – TheZuck

+8

El cartel original solo dice que está usando Mockito; solo se da a entender que Mockito es un requisito fijo y difícil, por lo que la sugerencia de que JMockit puede manejar esta situación no es tan inapropiado. – Bombe

6

Si no puede cambiar la variable miembro, luego la otra forma de evitar esto es usar 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 a la nueva Segunda devolverá la misma instancia burlado. Pero en tu caso simple, esto funcionará.

1

Muchos otros ya te han aconsejado que vuelvas a pensar tu código para que sea más comprobable, un buen consejo y generalmente más simple de lo que estoy por sugerir.

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

PowerMock extiende Mockito (por lo que no tiene que aprender un nuevo marco simulado), que proporciona una funcionalidad adicional. Esto incluye la capacidad de hacer que un constructor devuelva un simulacro. Potente, pero un poco complicado, así que úsalo juiciosamente.

Utiliza un corredor de simulacros diferente. Y debe preparar la clase que invocará al constructor. (Tenga en cuenta que este es un Gotcha común - preparar la clase que llama al constructor, no a la clase construida)

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

Luego, en el establecimiento de la prueba, puede utilizar el método whenNew tener el constructor devuelve una maqueta

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class)); 
5

Tuve el mismo problema cuando no se estableció un valor privado porque Mockito no llama a los super constructores. Aquí es cómo aumento la burla con reflexión.

Primero, creé una clase TestUtils que contiene muchas utilidades útiles que incluyen estos métodos de reflexión. El acceso de reflexión es un poco inestable para implementar cada vez. Creé estos métodos para probar código en proyectos que, por una razón u otra, no tenían ningún paquete de burla 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); 
     } 
    } 

} 

Luego puedo probar la clase con una variable privada como esta. Esto es útil para burlarse de los árboles profundos en 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é el código de mi proyecto real aquí, en la página. Podría haber una cuestión 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.

+0

¿Podría explicar su código con un uso real? como clase pública tobeMocker() { private ClassObject classObject; } Donde classObject es igual al objeto que se reemplazará. –

+0

En su ejemplo, si instancia de ToBeMocker = nuevo ToBeMocker(); y ClassObject someNewInstance = new ClassObject() { @Override // algo así como una dependencia externa }; luego TestUtils.refelctSetValue (instancia, "classObject", someNewInstance); Tenga en cuenta que debe averiguar qué desea anular para burlarse. Digamos que tienes una base de datos y esta anulación devolverá un valor para que no necesites seleccionar. Recientemente, tuve un bus de servicio que no quería procesar el mensaje, pero quería asegurarme de que lo recibiera. Por lo tanto, configuro la instancia del bus privado de esta manera: ¿Útil? – dave

+0

Tendrá que imaginar que había formateo en ese comentario. Fue eliminado. Además, esto no funcionará con Java 9 ya que bloqueará el acceso privado. Tendremos que trabajar con otras construcciones una vez que tengamos un lanzamiento oficial y podamos trabajar dentro de sus límites reales. – dave

Cuestiones relacionadas