2014-09-03 18 views
6

Nel namespace Microsoft.VisualStudio.TestTools.UnitTesting, è disponibile la comoda classe statica Assert per gestire le asserzioni in corso nei test.Microsoft.VisualStudio.TestTools.UnitTesting.Assert comportamento generico di overload dei metodi

Qualcosa che mi ha infastidito è che la maggior parte dei metodi sono estremamente sovraccarichi e, per di più, hanno una versione generica. Un esempio specifico è Assert.AreEqual che ha 18 sovraccarichi, tra i quali:

Assert.AreEqual<T>(T t1, T t2) 

Qual è l'uso di questo metodo generico? Inizialmente, pensavo che fosse un modo per chiamare direttamente il metodo IEquatable<T> Equals(T t), ma non è questo il caso; chiamerà sempre la versione non generica object.Equals(object other). Ho scoperto la via difficile dopo aver codificato alcuni test unitari in attesa di tale comportamento (invece di esaminare in anticipo la definizione della classe Assert come avrei dovuto).

Al fine di chiamare la versione generica del Equals, il metodo generico avrebbe dovuto essere definito come:

Assert.AreEqual<T>(T t1, T t2) where T: IEquatable<T> 

C'è un buon motivo per cui non è stato fatto in questo modo?

Sì, si perde il metodo generico per tutti quei tipi che non implementano IEquatable<T>, ma non è una grande perdita comunque come l'uguaglianza sarebbe visibile attraverso object.Equals(object other), quindi Assert.AreEqual(object o1, object o2) è già abbastanza buono.

L'attuale metodo generico offre vantaggi che non sto considerando, o è solo il caso che nessuno si è fermato a pensarci perché non è un granché? L'unico vantaggio che vedo è la sicurezza del tipo di argomento, ma sembra piuttosto scadente.

Edit: fissato un errore in cui ho continuato riferendosi al IComparable quando volevo dire IEquatable.

+0

Non è possibile vincolarlo, poiché qualsiasi T senza il proprio sovraccarico si risolverebbe naturalmente a tale metodo come parte della risoluzione di sovraccarico e solo allora il compilatore si lamenterà del vincolo non soddisfatto per qualsiasi T che non implementa l'interfaccia. Per quanto riguarda l'ipotesi errata, hai controllato la documentazione del metodo prima di costruire la strategia di test intorno ad essa? –

+0

AnthonyPegram: se lo si limita non si può passare in nessuna T che non implementa l'interfaccia, quindi come esattamente la risoluzione di sovraccarico si guasterebbe in runtime? – InBetween

+1

Fallirebbe al momento della compilazione, ma la risoluzione del sovraccarico avrebbe già deciso sull'overload T e * quindi * controllare il vincolo (e fallire). I vincoli non fanno parte della firma. Se cerchi questa frase, troverai molte risorse (qui e altrove, in particolare il blog di Eric Lippert) che ti forniranno maggiori dettagli al riguardo. –

risposta

4

Il metodo con questo vincolo non sarebbe ideale a causa del problema spesso affrontato di constraints not being part of the signature.

Il problema sarebbe che per ogni T che non è coperto da un proprio sovraccarico specifico, il compilatore sceglierebbe il metodo generico AreEqual<T> come la soluzione migliore durante la risoluzione di sovraccarico, come sarebbe effettivamente da una corrispondenza esatta. In una fase diversa del processo, il compilatore valuterà che T superi il vincolo. Per qualsiasi T che non implementa IEquatable<T>, questo controllo non funzionerà e il codice non verrà compilato.

Si consideri questo esempio semplificato del codice della libreria unit testing e una classe che possa esistere nella libreria:

public static class Assert 
{ 
    public static void AreEqual(object expected, object actual) { } 
    public static void AreEqual<T>(T expected, T actual) where T : IEquatable<T> { } 
} 

class Bar { } 

Classe Bar non implementa l'interfaccia nel vincolo. Se dovessimo poi aggiungere il seguente codice a una prova di unità

Assert.AreEqual(new Bar(), new Bar()); 

Il codice non riuscirebbe a compilare a causa del vincolo insoddisfatto sul metodo che è il candidato migliore. (Bar sostituti T, che lo rende un candidato migliore di object.)

Il tipo 'Bar' non può essere utilizzato come parametro di tipo 'T' nel tipo generico o metodo 'Assert.AreEqual <T> (T, T) '. Non vi è alcuna conversione implicita del riferimento da "Bar" a "System.IEquatable <Bar>".

Per soddisfare il compilatore e permettere il codice unità di prova per compilare ed eseguire, avremmo dovuto lanciare almeno un ingresso per il metodo a object modo che il sovraccarico non generico può essere scelto, e questo sarebbe vero per qualsiasi dato T che potrebbe esistere nel proprio codice o codice che si consuma che si desidera utilizzare nei casi di test che non implementano l'interfaccia.

Assert.AreEqual((object)new Bar(), new Bar()); 

Quindi la domanda deve essere posta - sarebbe l'ideale? Se stessimo scrivendo una libreria di test delle unità, creereste un tale metodo con una limitazione così ostile? Sospetto che non lo faresti, e nemmeno gli implementatori della libreria di test delle unità Microsoft (che fosse per questo motivo o meno).

+0

+1 Grazie per avermi fatto capire cosa sta succedendo. Mi chiedo però perché i vincoli di tipo non fanno parte della risoluzione di sovraccarico ... Inoltre mi chiedo perché il compilatore genera un errore invece di emettere un avvertimento e di ritornare all'oggetto 'AreEqual (oggetto atteso ... '. –

1

Fondamentalmente, il sovraccarico generico obbliga a confrontare due oggetti dello stesso tipo. Se si modifica il tipo di valore previsto o effettivo, verrà visualizzato l'errore di compilazione. Ecco MSDN blog descrivendolo abbastanza bene.

+0

Sì, questo è quello che menziono nella domanda ... l'unico vantaggio è la sicurezza del tipo di argomento, ma in questo caso, in questo caso, questo motivo è piuttosto scarso. – InBetween

1

Si può decompilare il metodo e vedere che tutto quello che in realtà non fa altro che aggiungere un controllo di tipo (via ILSpy), che non è nemmeno fatto correttamente a mio parere (controlla i tipi dopo l'uguaglianza):

public static void AreEqual<T>(T expected, T actual, string message, params object[] parameters) 
{ 
    message = Assert.CreateCompleteMessage(message, parameters); 
    if (!object.Equals(expected, actual)) 
    { 
     string message2; 
     if (actual != null && expected != null && !actual.GetType().Equals(expected.GetType())) 
     { 
      message2 = FrameworkMessages.AreEqualDifferentTypesFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), Assert.ReplaceNulls(expected), expected.GetType().FullName, Assert.ReplaceNulls(actual), actual.GetType().FullName); 
     } 
     else 
     { 
      message2 = FrameworkMessages.AreEqualFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), Assert.ReplaceNulls(expected), Assert.ReplaceNulls(actual)); 
     } 
     Assert.HandleFailure("Assert.AreEqual", message2); 
    } 
} 

In teoria, è possibile utilizzare EqualityComparer<T>.Default per utilizzare il parametro Equals generico, se disponibile, ma che farebbe il fallback su Equals non generico altrimenti. Ciò quindi non richiederebbe un vincolo a IEquatable<T>. Avere un comportamento diverso per i metodi Equals generici e non generici è un odore di codice, IMO.

Onestamente, il sovraccarico generico non è tremendamente utile a meno che non si digiti il ​​parametro generico. Non riesco a contare quante volte ho digitato in modo errato una proprietà o confrontato due proprietà di tipi diversi e abbiamo scelto il sovraccarico di AreEqual(object,object). Così mi dà un fallimento molto più tardi in fase di esecuzione piuttosto che compilare il tempo.

Problemi correlati