2011-04-18 20 views
6

¿Es aceptable hacer afirmaciones en sus devoluciones de llamada si luego verifica que se invocaron los métodos? ¿Es esta la forma preferida de asegurarme de que mi simulación obtenga los parámetros esperados, o debo establecer una variable local en mi devolución de llamada y hacer las afirmaciones en esa instancia?La forma correcta de verificar los parámetros que se pasan a un simulacro se establece como se esperaba

Tengo una situación en la que tengo cierta lógica en una clase Presenter que deriva valores basados ​​en entradas y los pasa a una clase Creator. Para probar la lógica en la clase Presenter, quiero verificar que se observen los valores derivados adecuados cuando se llame al Creador. Se me ocurrió el siguiente ejemplo que funciona, pero no estoy seguro de si me gusta este enfoque:

[TestFixture] 
public class WidgetCreatorPresenterTester 
{ 
    [Test] 
    public void Properly_Generates_DerivedName() 
    { 
     var widgetCreator = new Mock<IWidgetCreator>(); 
     widgetCreator.Setup(a => a.Create(It.IsAny<Widget>())) 
        .Callback((Widget widget) => 
        Assert.AreEqual("Derived.Name", widget.DerivedName)); 

     var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
     presenter.Save("Name"); 

     widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once()); 
    } 
} 

que a mí respecta, ya que sin la llamada Verify al final, no hay garantía de que la aserción en la devolución de llamada sería invocada. Otro enfoque sería establecer una variable local en la devolución de llamada:

[Test] 
public void Properly_Generates_DerivedName() 
{ 
    var widgetCreator = new Mock<IWidgetCreator>(); 
    Widget localWidget = null; 
    widgetCreator.Setup(a => a.Create(It.IsAny<Widget>())) 
     .Callback((Widget widget) => localWidget = widget); 

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
    presenter.Save("Name"); 

    widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once()); 
    Assert.IsNotNull(localWidget); 
    Assert.AreEqual("Derived.Name", localWidget.DerivedName); 
} 

siento que este enfoque es menos propenso a errores, ya que es más explícito, y es fácil ver que las declaraciones Assert serán llamados. ¿Un enfoque es preferible al otro? ¿Hay alguna manera más sencilla de probar el parámetro de entrada pasado a un simulacro que me falta?

En caso de que sea útil, aquí está el resto del código para este ejemplo:

public class Widget 
{ 
    public string Name { get; set; } 
    public string DerivedName { get; set; } 
} 

public class WidgetCreatorPresenter 
{ 
    private readonly IWidgetCreator _creator; 

    public WidgetCreatorPresenter(IWidgetCreator creator) 
    { 
     _creator = creator; 
    } 

    public void Save(string name) 
    { 
     _creator.Create(
      new Widget { Name = name, DerivedName = GetDerivedName(name) }); 
    } 

    //This is the method I want to test 
    private static string GetDerivedName(string name) 
    { 
     return string.Format("Derived.{0}", name); 
    } 
} 

public interface IWidgetCreator 
{ 
    void Create(Widget widget); 
} 

EDITAR
He actualizado el código para hacer que el segundo enfoque esbocé en la pregunta más fácil de usar . Saqué la creación de la expresión utilizada en Configuración/Verificar en una variable separada, así que solo tengo que definirla una vez. Siento que este método es lo que más me gusta, es fácil de configurar y falla con buenos mensajes de error.

[Test] 
public void Properly_Generates_DerivedName() 
{ 
    var widgetCreator = new Mock<IWidgetCreator>(); 
    Widget localWidget = null; 

    Expression<Action<IWidgetCreator>> expressionCreate = 
     (w => w.Create(It.IsAny<Widget>())); 
    widgetCreator.Setup(expressionCreate) 
     .Callback((Widget widget) => localWidget = widget); 

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
    presenter.Save("Name"); 

    widgetCreator.Verify(expressionCreate, Times.Once()); 
    Assert.IsNotNull(localWidget); 
    Assert.AreEqual("Derived.Name", localWidget.DerivedName); 
} 

Respuesta

3

Debido a la forma en que se estructura el código, se verá forzado a probar dos cosas en una prueba unitaria. Está probando que A) su presentador está llamando al método de creación del WidgetCreator inyectado y B) que el nombre correcto está establecido en el nuevo Widget. Si es posible, sería mejor si de alguna manera puedes hacer que estas dos cosas sean dos pruebas separadas, pero en este caso realmente no veo una manera de hacerlo.

Dado todo eso, creo que el segundo enfoque es más limpio. Es más explícito en cuanto a lo que estás esperando, y si falla, tendría perfecto sentido por qué y dónde está fallando.

+0

Estoy de acuerdo en que el segundo enfoque es preferible. Me gusta la sugerencia de @aqwert de usar Verify, pero los mensajes de error son demasiado difíciles de usar. Agregué un código a la pregunta original que actualiza el segundo enfoque, por lo que solo debo declarar la expresión una vez. Creo que me quedaré con ese enfoque. – rsbarro

4

Lo que hago es hacer Verify con coincidencias con AAA. Y debido a esto, la configuración no es necesaria. Puedes alinearlo pero lo separé para que se vea más limpio.

[Test] 
public void Properly_Generates_DerivedName() 
{ 
    var widgetCreator = new Mock<IWidgetCreator>(); 

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object); 
    presenter.Save("Name"); 

    widgetCreator.Verify(a => a.Create(MatchesWidget("Derived.Name")); 
} 

private Widget MatchesWidget(string derivedName) 
{ 
    return It.Is<Widget>(m => m.DerivedName == derivedName); 
} 
+0

+1 Gracias por la respuesta. Es mucho más fácil de leer que mis ejemplos, pero los mensajes de error son un poco extraños para una prueba unitaria. En lugar de mensajes de "esperado, pero era", obtienes "Invocación esperada en el simulacro al menos una vez, pero nunca se realizó". No es demasiado difícil de manejar una vez que te acostumbras, pero tal vez no estés muy claro si no estás acostumbrado a ver ese tipo de mensaje. – rsbarro

+1

Sí, los mensajes del marco de burla pueden ser un poco extraños. Puede agregar un mensaje de error al método 'Verificar' si eso ayuda – aqwert

+0

Sí, el mensaje de error ayuda. Voy a dejar esta pregunta abierta por un tiempo para ver si hay otras sugerencias. Gracias de nuevo. – rsbarro

3

Sólo para elaborar en comentario de @ rsbarro - el mensaje de error fracaso Moq:

esperado invocación en el simulacro al menos una vez, pero nunca se llevó a cabo

... es menos útil para tipos complejos, al determinar exactamente la condición which que realmente falló, al buscar un error (ya sea en el código o en la prueba unitaria).

menudo me encuentro cuando utilice Moq Verify para verificar un gran número de condiciones en el Verify, en el que el método debe haber sido llamados con valores de los parámetros específicos que no son primitivas como int o string. (Esto no suele ser un problema para los tipos primitivos, ya que Moq enumera las "invocaciones realizadas" reales en el método como parte de la excepción).

Como resultado, en este caso, necesitaría para capturar los parámetros pasados ​​(que me parece duplicar el trabajo de Moq), o simplemente mover la línea de aserción con Setup/Callbacks.

p. Ej. Verificación:

widgetCreator.Verify(wc => wc.Create(
     It.Is<Widget>(w => w.DerivedName == "Derived.Name" 
        && w.SomeOtherCondition == true), 
     It.Is<AnotherParam>(ap => ap.AnotherCondition == true), 
    Times.Exactly(1)); 

se recodificará como

widgetCreator.Setup(wc => wc.Create(It.IsAny<Widget>(), 
            It.IsAny<AnotherParam>()) 
      .Callback<Widget, AnotherParam>(
       (w, ap) => 
       { 
        Assert.AreEqual("Derived.Name", w.DerivedName); 
        Assert.IsTrue(w.SomeOtherCondition); 
        Assert.IsTrue(ap.AnotherCondition, "Oops"); 
       }); 

// *** Act => invoking the method on the CUT goes here 

// Assert + Verify - cater for rsbarro's concern that the Callback might not have happened at all 
widgetCreator.Verify(wc => wc.Create(It.IsAny<Widget>(), It.Is<AnotherParam>()), 
    Times.Exactly(1)); 

A primera vista, esto viola AAA, ya que estamos poniendo la línea Assert con el Arrange (aunque la devolución de llamada sólo es invocarse durante el Act), pero al menos podemos llegar al fondo del problema.

Véase también la idea de Hady de mover el lambda de devolución de llamada 'seguimiento' en su propia función con nombre, o mejor aún, en C# 7, esto se puede mover a una Local Function en la parte inferior del método de prueba de la unidad, por lo que el AAA diseño puede ser retenido.

1

Basándose en la cima de la respuesta StuartLC 's en este hilo, que sigue lo que sugiere sin violar AAA escribiendo una función de 'en línea' que se pasa al método de un objeto de burla Verify.

Así, por ejemplo:

// Arrange 
widgetCreator 
    .Setup(wc => wc.Create(It.IsAny<Widget>(), It.IsAny<AnotherParam>()); 

// Act 
// Invoke action under test here... 

// Assert 
Func<Widget, bool> AssertWidget = request => 
{ 
    Assert.AreEqual("Derived.Name", w.DerivedName); 
    Assert.IsTrue(w.SomeOtherCondition); 
    Assert.IsTrue(ap.AnotherCondition, "Oops"); 
    return true; 
}; 

widgetCreator 
    .Verify(wc => wc.Create(It.Is<Widget>(w => AssertWidget(w)), It.Is<AnotherParam>()), Times.Exactly(1)); 
Cuestiones relacionadas