2009-11-19 7 views
7

Acabo de empezar a usar objetos simulados (usando el mockito de Java) en mis pruebas recientes. Huelga decir que simplificaron la parte de configuración de las pruebas, y junto con Dependency Injection, yo diría que hizo que el código sea aún más robusto.Uso de simulacros en las pruebas

Sin embargo, me encontré tropezando en las pruebas contra la implementación en lugar de las especificaciones. Terminé estableciendo expectativas de que yo argumentaría que no es parte de las pruebas. En términos más técnicos, probaré la interacción entre el SUT (la clase bajo prueba) y sus colaboradores, ¡y esa dependencia no forma parte del contrato o la interfaz de la clase!

Considere que tiene lo siguiente: Cuando se trata de nodo XML, suponga que tiene un método, attributeWithDefault() que devuelve el valor del atributo del nodo si está disponible, de lo contrario se devolverá un valor por defecto!

lo haría configuración de la prueba como la siguiente:

Element e = mock(Element.class); 

when(e.getAttribute("attribute")).thenReturn("what"); 
when(e.getAttribute("other")).thenReturn(null); 

assertEquals(attributeWithDefault(e, "attribute", "default"), "what"); 
assertEquals(attributeWithDefault(e, "other", "default"), "default"); 

Bueno, aquí no sólo no prueba que attributeWithDefault() se adhiere a la especificación, pero también puso a prueba la aplicación, ya que obligaba a utilizar Element.getAttribute(), en lugar de Element.getAttributeNode().getValue() o Element.getAttributes().getNamedItem().getNodeValue(), etc.

Supongo que lo estoy haciendo de la manera incorrecta, así que cualquier consejo sobre cómo puedo mejorar mi uso de los simulacros y las mejores prácticas será apreciado.

EDIT: lo que está mal con la prueba de

hice la suposición anterior de que la prueba es un mal estilo, aquí está mi razón de ser.

  1. La especificación no especifica qué método se llama. Un cliente de la biblioteca no debería preocuparse por cómo se recupera el atributo, por ejemplo, siempre que se haga correctamente. El implementador debe tener libertad para acceder a cualquiera de los enfoques alternativos, de la forma que considere adecuada (con respecto al rendimiento, la coherencia, etc.). Es la especificación de Element que asegura que todos estos enfoques devuelven valores idénticos.

  2. No tiene sentido volver a factorizar Element en una sola interfaz de método con getElement() (Go es bastante bueno acerca de esto en realidad). Para facilitar el uso, un cliente del método debería ser capaz de utilizar el estándar Element en la biblioteca estándar. Tener interfaces y nuevas clases es simplemente una tontería, en mi humilde opinión, ya que hace que el código del cliente sea desagradable, y no vale la pena.

  3. Suponiendo que la especificación se mantiene como está y la prueba se mantiene como está, un nuevo desarrollador puede decidir refactorizar el código para usar un enfoque diferente de usar el estado y hacer que la prueba falle. Bueno, una prueba que falla cuando la implementación real se adhiere a la especificación es válida.

  4. Tener un estado de exposición del colaborador en múltiples formatos es bastante común. Una especificación y la prueba no deben depender de qué enfoque particular se tome; ¡solo la implementación debería!

Respuesta

6

Este es un problema común en las pruebas de simulacro, y el mantra en general a alejarse de esto es:

Only mock types you own.

aquí si quieres para burlarse de colaboración con un analizador XML (no necesariamente necesario, honestamente, como un pequeño XML prueba debería funcionar bien en un contexto unidad) entonces que analizador XML debe estar detrás de una interfaz o clase que usted posee que se ocupará de los detalles desordenados de qué método en la API de terceros debe llamar. El punto principal es que tiene un método que obtiene un atributo de un elemento. Burlarse de ese método. Esto separa la implementación del diseño. La implementación real tendría una prueba de unidad real que realmente prueba que obtiene un elemento exitoso de un objeto real.

Los mocks pueden ser una buena manera de guardar el código de configuración estándar (actuando esencialmente como Stubs), pero ese no es su propósito central en términos de diseño de conducción. Los simulacros son un comportamiento de prueba (a diferencia del estado) y son not Stubs.

Debo agregar que cuando usa Mocks como talones, se parecen a su código. Cualquier resguardo tiene que hacer suposiciones sobre cómo lo va a llamar que están vinculadas a su implementación. Eso es normal. Donde hay un problema es si eso está conduciendo su diseño de malas maneras.

+1

+1. Al igual que la forma en que aclaró la diferencia entre las pruebas de comportamiento (burlas) y estado (pruebas). – notnoop

0

La única solución que puedo ver por aquí (y tengo que admitir que no estoy familiarizado con la biblioteca que está utilizando) es crear un elemento de simulacro que tiene toda la funcionalidad incluida, que es, también tiene la capacidad de establecer el valor de getAttributeNote(). getValue() y getAttributes(). getNamedItem(). getNodeValue().

Pero, suponiendo que todas sean equivalentes, está bien simplemente probar una. Es cuando varía que necesita probar todos los casos.

1

Al diseñar las pruebas unitarias, siempre se probará efectivamente su implementación, y no alguna especificación abstracta. O puede argumentarse que probará la "especificación técnica", que es la especificación empresarial ampliada con detalles técnicos. No hay nada malo en esto. En lugar de probar eso:

Mi método devolverá un valor si está definido o es un valor predeterminado.

que está probando:

Mi método devolverá un valor si se ha definido o un incumplimiento previsto que el elemento XML suministrado volverá este atributo cuando llamo getAttribute (nombre).

+0

Estás en lo cierto al descubrir lo que se está probando. Sin embargo, yo sostengo que el contrato público del método no debería ser tan estricto. – notnoop

0

No encuentro nada de malo en su uso de los simuladores. Lo que está probando es el método attributeWithDefault() y su implementación, no si Element es correcto o no. Entonces se burló de Element para reducir la cantidad de configuración requerida. La prueba asegura que la implementación de attributeWithDefault() se ajusta a la especificación, naturalmente, debe haber alguna implementación específica que se pueda ejecutar para la prueba.

0

Aquí está probando su objeto de prueba. Si desea probar el método attributeWithDefault(), debe afirmar que e.getAttribute() recibe una llamada con el argumento esperado y olvida el valor devuelto. Este valor de retorno solo verifica la configuración de su objeto simulado. (No sé cómo se hace exactamente con el mockito de Java, soy un tipo puro de C# ...)

+1

Me parece que la prueba verifica que la clase bajo prueba cambia su comportamiento dependiendo del valor de retorno de 'e.getAttribute()'. No está probando que el simulacro arroja valores correctos, sino que el comportamiento de clase cambia en función del valor de retorno. – Yishai

0

Depende de si obtener el atributo mediante la invocación de getAttribute() es parte de la especificación, o si se trata de un detalle de implementación que podría cambiar.

Si Element es una interfaz, decir que debe usar 'getAttribute' para obtener el atributo es probablemente parte de la interfaz. Entonces tu prueba está bien.

Si Element es una clase concreta, pero attributeWithDefault no debe ser consciente de cómo se puede obtener el atributo, entonces tal vez haya una interfaz esperando aparecer aquí.

public interface AttributeProvider { 
    // Might return null 
    public String getAttribute(String name); 
} 

public class Element implements AttributeProvider { 
    public String getAttribute(String name) { 
     return getAttributeHolder().doSomethingReallyTricky().toString(); 
    } 
} 

public class Whatever { 
    public String attributeWithDefault(AttributeProvider p, String name, String default) { 
    String res = p.getAtribute(name); 
    if (res == null) { 
     return default; 
    } 
    } 
} 

Luego, se probará attributeWithDefault contra un Mock AttributeProvider en lugar de un Element.

Por supuesto, en esta situación, probablemente sería una exageración, y su prueba probablemente sea buena incluso con una implementación (tendrá que probarla en algún lugar de todos modos;)). Sin embargo, este tipo de desacoplamiento puede ser útil si la lógica llega a ser más complicada, ya sea en getAttribute o en attributeWithDefualt.

Esperando que esto ayude.

+0

Estoy de acuerdo contigo es una exageración. Actualicé la publicación. – notnoop

+0

Menciona "una sola interfaz con getElement"; ¿te refieres a "una sola interfaz con getAttribute"? Después de su actualización: si entiendo correctamente, quiere proteger el atributo WithDefault de saber realmente cómo obtiene un atributo de un elemento. Lo entiendo, y otra forma de hacer cumplir esto sería agregar otro nivel de indirección (en lugar de pasar el Elemento, pasas otro objeto que sabe cómo obtener el atributo de un elemento). Sin embargo, creo que, no importa qué, en algún momento necesitarás una clase que * sepa * cómo obtener un atributo de un elemento. – phtrivier

0

Me parece que hay 3 cosas que desea verificar con este método:

  1. Se pone el atributo desde el lugar correcto (Element.getAttribute())
  2. Si el atributo no es nulo, se devuelve
  3. Si el atributo es nulo, la cadena "por defecto" se devuelve

Estás verificación # 2 y # 3, pero no # 1. Con mockito, puede verificar el n. ° 1 al agregar

verify(e.getAttribute("attribute")); 
verify(e.getAttribute("other")); 

Lo que asegura que los métodos se están llamando en su simulacro. Es cierto que esto es un poco torpe en mockito. En EasyMock, usted hacer algo como:

expect(e.getAttribute("attribute")).andReturn("what"); 
expect(e.getAttribute("default")).andReturn(null); 

Tiene el mismo efecto, pero creo que hace que su prueba un poco más fácil de leer.

+0

@chrispix. Hay tres "lugares correctos" para obtener el atributo. La prueba que escribí solo verifica que se usa uno de ellos. La implementación es libre de elegir cualquiera. La prueba no debería importar dónde se recupera. – notnoop

+0

Si desea que la prueba sea independiente de cómo su clase colabora con su dependencia, no debe burlarse de la dependencia, sino simplemente usar un fragmento XML de prueba, como sugiere Yishai. Dependiendo de qué tan compleja sea su clase, eso puede no ser realista: administrar los datos XML de prueba (u otros datos estructurados) puede ser una gran molestia y hacer que sus pruebas sean frágiles. –

+0

Tampoco creo que sea un gran problema si se rompe la prueba cuando alguien refactoriza la implementación. Su refactorización para usar un método diferente puede ser válida, pero también es fácil actualizar la prueba. En ese caso, la prueba de falla sirve como advertencia para asegurarse de que la refactorización sea realmente válida. He hecho que los desarrolladores refaccionen una implementación de una manera que pensaban que se adhería a la especificación, pero estaban equivocados. Una mejor prueba habría expuesto el error. –

0

Si está utilizando la inyección de dependencia, los colaboradores deberían formar parte del contrato. Debe poder inyectar a todos los colaboradores a través del constructor o una propiedad pública.

En pocas palabras: si tiene un colaborador que está renovando en lugar de inyectar, es probable que necesite refactorizar el código. Este es un cambio de mentalidad necesario para probar/burlarse/inyectar.

+0

Además, cuando miro su ejemplo de código específico, no está especificando cómo se está transfiriendo 'Element e' a su método. ¿Se inyecta en el objeto o se pasa como un parámetro? En este caso, podría tener más sentido utilizar un objeto concreto con la configuración de datos para que coincida con su caso de prueba en lugar de utilizar un simulacro. – Brett

+0

¿Quiere decir que al adoptar 'DI', necesitaría especificar explícitamente cómo y qué métodos se llaman. Cuando la lista de especificaciones de los estados no está vacía, ¿debo aclarar que llamaré a '! List.isEmpty()' en lugar de a 'list.size()! = 0'. ¿Eso no contaminaría el contrato? – notnoop

+0

@Brett, supongo que es un método estático. – notnoop

0

Esta es una respuesta tardía, pero toma un punto de vista diferente de los demás.

Básicamente, el OP tiene razón al pensar que la prueba con burla es mala, por las razones que indicó en la pregunta. Los que dicen que los simulacros están bien no han proporcionado buenas razones para ello, IMO.

Aquí está una versión completa de la prueba, en dos versiones: una con burla (la MALA) y otra sin (la BUENA). (Me tomé la libertad de usar una biblioteca de burlas diferente, pero eso no cambia el punto.)

import javax.xml.parsers.*; 
import org.w3c.dom.*; 
import org.junit.*; 
import static org.junit.Assert.*; 
import mockit.*; 

public final class XmlTest 
{ 
    // The code under test, embedded here for convenience. 
    public static final class XmlReader 
    { 
     public String attributeWithDefault(
      Element xmlElement, String attributeName, String defaultValue 
     ) { 
      String attributeValue = xmlElement.getAttribute(attributeName); 
      return attributeValue == null || attributeValue.isEmpty() ? 
       defaultValue : attributeValue; 
     } 
    } 

    @Tested XmlReader xmlReader; 

    // This test is bad because: 
    // 1) it depends on HOW the method under test is implemented 
    // (specifically, that it calls Element#getAttribute and not some other method 
    //  such as Element#getAttributeNode) - it's therefore refactoring-UNSAFE; 
    // 2) it depends on the use of a mocking API, always a complex beast which takes 
    // time to master; 
    // 3) use of mocking can easily end up in mock behavior that is not real, as 
    // actually occurred here (specifically, the test records Element#getAttribute 
    // as returning null, which it would never return according to its API 
    // documentation - instead, an empty string would be returned). 
    @Test 
    public void readAttributeWithDefault_BAD_version(@Mocked final Element e) { 
     new Expectations() {{ 
      e.getAttribute("attribute"); result = "what"; 

      // This is a bug in the test (and in the CUT), since Element#getAttribute 
      // never returns null for real. 
      e.getAttribute("other"); result = null; 
     }}; 

     String actualValue = xmlReader.attributeWithDefault(e, "attribute", "default"); 
     String defaultValue = xmlReader.attributeWithDefault(e, "other", "default"); 

     assertEquals(actualValue, "what"); 
     assertEquals(defaultValue, "default"); 
    } 

    // This test is better because: 
    // 1) it does not depend on how the method under test is implemented, being 
    // refactoring-SAFE; 
    // 2) it does not require mastery of a mocking API and its inevitable intricacies; 
    // 3) it depends only on reusable test code which is fully under the control of the 
    // developer(s). 
    @Test 
    public void readAttributeWithDefault_GOOD_version() { 
     Element e = getXmlElementWithAttribute("what"); 

     String actualValue = xmlReader.attributeWithDefault(e, "attribute", "default"); 
     String defaultValue = xmlReader.attributeWithDefault(e, "other", "default"); 

     assertEquals(actualValue, "what"); 
     assertEquals(defaultValue, "default"); 
    } 

    // Creates a suitable XML document, or reads one from an XML file/string; 
    // either way, in practice this code would be reused in several tests. 
    Element getXmlElementWithAttribute(String attributeValue) { 
     DocumentBuilder dom; 
     try { dom = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } 
     catch (ParserConfigurationException e) { throw new RuntimeException(e); } 
     Element e = dom.newDocument().createElement("tag"); 
     e.setAttribute("attribute", attributeValue); 
     return e; 
    } 
} 
Cuestiones relacionadas