2016-02-15 89 views
6

Ho un problema interessante da risolvere. Prendi in considerazione un'interfaccia come questa:Come imitare `object.Equals (object obj)` per un'interfaccia usando Moq

public interface IMyThing 
{ 
    int Id { get; } 
} 

Ora voglio testare il codice che utilizza questa interfaccia. Forse qualcosa con qualche magia LINQ. Qualcosa di simile a questo:

public class SomeClass 
{ 
    private IMyThing _thing; 

    ... 

    public bool HasThing(IEnumerable<IMyThing> things) 
    { 
     return things.Contains(_thing); 
    } 
} 

sto beffardo tutti gli oggetti attuazione IMyThing utilizzando Moq:

public static IMyThing MockMyThing(int newId) 
{ 
    var mock = new Mock<IMyThing>(); 
    mock.Setup(s => s.Id).Returns(newId); 
    mock.Setup(s => s.Equals(It.IsAny<object>())).Returns<object>(obj => 
    { 
     if (typeof(IMyThing).IsAssignableFrom(obj.GetType())) 
     { 
      return ((IMyThing)obj).Id == newId; 
     } 

     return false; 
    }); 

    return mock.Object; 
} 

Ecco la cosa. Il codice sopra riportato viene compilato senza avvisi ma non funzionerà mai. Moq crea un intercettore per il metodo Equals() ma non viene mai raggiunto. Invece, viene chiamato il metodo equals del proxy dell'oggetto. Sto dando la colpa al fatto che sto prendendo in giro un'interfaccia ma non una lezione concreta.

UPDATE: Appena realizzato Moq non crea nemmeno un intercettore.

Naturalmente potrei aumentare l'interfaccia IMyThing in questo modo:

public interface IMyThing : IEquatable<IMyThing> 
{ 
    int Id { get; } 
} 

L'operatore LINQ avrebbe riconosciuto l'interfaccia IEquatable<T> e utilizzarlo.

Non voglio fare questo perché:

  • Questa è utilizzabile solo con altri oggetti IMyThing
  • IEquatable<T> non era destinato a questo scopo
  • io non voglio macchiare il mio modello solo per renderlo mockable

Come risolverebbe questo?

+0

Se si sta tentando di testare alcuni scenari complessi durante il metodo 'HasThing()', forse dovrebbe accettare 'IEnumerable '. Perché non "introdurre un ulteriore livello di riferimento indiretto"? –

risposta

1

Ho finito per contribuire con il codice al progetto Moq su Github (vedere issue #248). Con questi cambiamenti è possibile prendere in giro object.Equals(object obj), object.GetHashCode() e object.ToString(), anche per le interfacce.

Consente di vedere se viene accettato.

+1

Il contributo è stato accettato! Evviva! – koloman

0

Forse non capisco completamente ma non vedo una funzione extando o overide Equals() per IMyThing? Quindi immagino che il mio primo approccio sarà overide, o extandes function, se non funziona lo farò IMyThing : IEquatable<IMyThing>, so che lo IEquatable<T> non era pensato per questo scopo ma lo chiudeva. e l'ultimo e non penso che è necessario farlo, ma esistono, basta creare una funzione come bool IsEquals(IMyThing other){//check if equals}

+1

Non c'è override concreto di 'Equals()' perché non esiste un'implementazione concreta di 'IMyThing'. Sto usando il mocking framework 'Moq' per ** fingere ** c'è un'implementazione se' IMyThing'. Pertanto semplicemente sovrascrivere 'Equals()' non funzionerebbe. L'implementazione di alcuni metodi 'IsEqual()' non funzionerebbe per gli stessi motivi. – koloman

1

Credo che il problema deriva dal fatto che il metodo Equals non è l'interfaccia che si sta prendendo in giro. Se crei una classe con un metodo sovrascrivibile Equals (anche se non fa nulla), allora sei in grado di deriderlo.

public class MyTestThing : IMyThing 
{ 
    public virtual int Id { get; } 

    public override bool Equals(object obj) 
    { 
     return base.Equals(obj); 
    } 
} 

[TestCase(55)] 
public void Can_mock_equals_method(int newId) 
{ 
    var mockThing = new Mock<MyTestThing>(); 

    mockThing.Setup(t => t.Id).Returns(newId); 
    mockThing.Setup(t => t.Equals(It.IsAny<object>())) 
     .Returns<object>(t => (t as IMyThing)?.Id == newId); 

    Assert.That(mockThing.Object.Equals(new MyRealThing(newId))); 
} 

Si noti che se si commento il metodo Equals su MyTestThing questo test fallirà, perché Moq non può più prendersi gioco di esso.

Se si intende creare classi di test come questa, potrebbe essere più utile implementare completamente Equals nella classe stessa, quindi non è necessario impostarlo su Moq.Potresti anche fare un ulteriore passo avanti e creare una classe base astratta in cui l'unico metodo implementato è Equals e usarlo sia nei tuoi test con Moq, sia derivarne la vera implementazione.

+0

Questo funzionerebbe. Diventa scomodo non appena IMyThing ha numerosi membri e numerose implementazioni in numerosi assembly. Essere in grado di prendere in giro il comportamento delle interfacce sarebbe molto più conveniente. – koloman

1

Se il modo in cui si eseguono i confronti può cambiare, non è necessario utilizzare confronti diretti, ma utilizzare oggetti che possono fare i confronti per voi.

Considerare la possibilità di modifiche alla classe in questo modo:

public class SomeClass 
{ 
    private IMyThing _thing; 

    public bool HasThing(IEnumerable<IMyThing> things, IEqualityComparer<IMyThing> comparer = null) 
    { 
     comparer = comparer ?? EqualityComparer<IMyThing>.Default; 
     return things.Contains(_thing, comparer); 
    } 
} 

Poi basta prendere in giro l'operatore di confronto pure.

+0

Questa è davvero una soluzione interessante! Ma il chiamante di 'HasThing (...)' non conosce necessariamente l'implementazione di 'IMyThing' stesso. Pertanto, non sa naturalmente quale comparatore di uguaglianza usare. Pertanto, mentre questo risolve il mio problema di test, rende il codice effettivo più complesso del necessario. Alla fine il confronto tra le implementazioni concrete di IMyThing non è affatto soggetto a cambiamenti. Sei d'accordo? – koloman