2009-01-28 33 views
33

Durante la creazione di un test unitario per una classe che utilizza lo HttpContext.Current.Cache class, viene visualizzato un errore quando si utilizza NUnit. La funzionalità è essenziale - controllare se un elemento è nella cache, e se non, crearlo e metterlo in:Unit test HttpContext.Current.Cache o altri metodi sul lato server in C#?

if (HttpContext.Current.Cache["Some_Key"] == null) { 
    myObject = new Object(); 
    HttpContext.Current.Cache.Insert("Some_Key", myObject); 
} 
else { 
    myObject = HttpContext.Current.Cache.Get("Some_Key"); 
} 

Quando si chiama questo da un test di unità, non riesce con a NullReferenceException quando incontra il primo Cache linea. In Java, vorrei usare Cactus per testare il codice lato server. C'è uno strumento simile che posso usare per il codice C#? This SO question cita framework mock: è l'unico modo in cui posso testare questi metodi? C'è uno strumento simile per eseguire test per C#?

Inoltre, non è possibile verificare se lo Cache è nullo poiché non desidero scrivere codice specifico per il test dell'unità e presupporre che sarà sempre valido durante l'esecuzione su un server. È valido o dovrei aggiungere controlli nulli sulla cache?

risposta

42

Il modo per farlo è evitare l'uso diretto di HttpContext o di altre classi simili e sostituirli con i mock. Dopo tutto, non stai provando a testare che HttpContext funzioni correttamente (questo è il lavoro di microsoft), stai solo provando a verificare che i metodi siano stati chiamati quando avrebbero dovuto.

Steps (Nel caso in cui si desidera solo conoscere la tecnica senza scavare attraverso i carichi di blog):

  1. Creare un'interfaccia che descrive i metodi che si desidera utilizzare nel vostro caching (probabilmente le cose come GetItem, SetItem, ExpireItem). Chiamatelo iCache o quello che vuoi

  2. Creare una classe che implementa l'interfaccia, e passa i metodi fino alla vera e propria HttpContext

  3. Creare una classe che implementa la stessa interfaccia, e solo agisce come una cache finta. Può utilizzare un dizionario o qualcosa del genere se si preoccupa di salvare gli oggetti

  4. Modificare il codice originale in modo che non utilizzi affatto HttpContext e utilizzi sempre solo un ICache.Il codice avrà quindi bisogno di ottenere un'istanza di ICache - puoi passare un'istanza nel tuo costruttore di classi (questa è tutta l'iniezione di dipendenza in realtà lo è), o inserirla in qualche variabile globale.

  5. Nella tua app di produzione, imposta l'ICache come la tua vera HttpContext-Backed-Cache, e nei tuoi test di unità, imposta l'ICache come mock cache.

  6. Utile!

2

Il consenso generale sembra essere che guidare qualsiasi cosa collegata a HttpContext dall'interno di un test di unità è un incubo totale, e dovrebbe essere evitato se possibile.

Penso che tu sia sulla strada giusta per quanto riguarda il beffardo. Mi piace RhinoMocks (http://ayende.com/projects/rhino-mocks.aspx).

Ho letto alcune cose positive anche su MoQ (http://code.google.com/p/moq), anche se non l'ho ancora provato.

Se davvero si vuole scrivere unità testabili web interfacce utente in C#, il modo in cui le persone sembrano essere voce è quello di utilizzare il framework MVC (http://www.asp.net/mvc) piuttosto che WebForms ...

6

Se si sta utilizzando .NET 3.5, è possibile utilizzare System.Web.Abstractions nella propria applicazione.

Justin Etheredge ha un ottimo post su come prendere in giro HttpContext (che contiene la classe cache).

Dall'esempio di Justin, passo un HttpContextBase ai miei controller usando HttpContextFactory.GetHttpContext. Quando li prendi in giro, costruisco un Mock per effettuare chiamate all'oggetto cache.

+6

È possibile prendere in giro quasi tutto con HttpContextBase, ma la proprietà Cache non è tra questi. HttpContextBase.Cache è di tipo System.Web.Caching.Cache che è sigillato, inutilizzabile nei test di unità e non mockable ... – chris166

0

Tutte queste domande di programmazione richiedono un modello di programmazione basato su interfaccia, in cui si implementa l'interfaccia due volte. Uno per il codice reale e uno per il mockup.

L'istanziazione è quindi il prossimo problema. Ci sono diversi modelli di design che possono essere utilizzati per questo. Vedi ad esempio i famosi modelli di GangOfFour Creation (GOF) oi modelli di Iniezione di dipendenza.

ASP.Net MVC sta infatti utilizzando questo approccio basato su interfaccia ed è quindi molto più adatto per il test dell'unità.

28

Sono d'accordo con gli altri che l'utilizzo di un'interfaccia sarebbe l'opzione migliore ma a volte non è possibile modificare un sistema esistente. Ecco un codice che ho appena passato insieme da uno dei miei progetti che dovrebbe darti i risultati che stai cercando. È la cosa più lontana da una bella o una grande soluzione, ma se davvero non puoi cambiare il tuo codice allora dovrebbe portare a termine il lavoro.

using System; 
using System.IO; 
using System.Reflection; 
using System.Text; 
using System.Threading; 
using System.Web; 
using NUnit.Framework; 
using NUnit.Framework.SyntaxHelpers; 

[TestFixture] 
public class HttpContextCreation 
{ 
    [Test] 
    public void TestCache() 
    { 
     var context = CreateHttpContext("index.aspx", "http://tempuri.org/index.aspx", null); 
     var result = RunInstanceMethod(Thread.CurrentThread, "GetIllogicalCallContext", new object[] { }); 
     SetPrivateInstanceFieldValue(result, "m_HostContext", context); 

     Assert.That(HttpContext.Current.Cache["val"], Is.Null); 

     HttpContext.Current.Cache["val"] = "testValue"; 
     Assert.That(HttpContext.Current.Cache["val"], Is.EqualTo("testValue")); 
    } 

    private static HttpContext CreateHttpContext(string fileName, string url, string queryString) 
    { 
     var sb = new StringBuilder(); 
     var sw = new StringWriter(sb); 
     var hres = new HttpResponse(sw); 
     var hreq = new HttpRequest(fileName, url, queryString); 
     var httpc = new HttpContext(hreq, hres); 
     return httpc; 
    } 

    private static object RunInstanceMethod(object source, string method, object[] objParams) 
    { 
     var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; 
     var type = source.GetType(); 
     var m = type.GetMethod(method, flags); 
     if (m == null) 
     { 
      throw new ArgumentException(string.Format("There is no method '{0}' for type '{1}'.", method, type)); 
     } 

     var objRet = m.Invoke(source, objParams); 
     return objRet; 
    } 

    public static void SetPrivateInstanceFieldValue(object source, string memberName, object value) 
    { 
     var field = source.GetType().GetField(memberName, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance); 
     if (field == null) 
     { 
      throw new ArgumentException(string.Format("Could not find the private instance field '{0}'", memberName)); 
     } 

     field.SetValue(source, value); 
    } 
} 
0

Come tutti dicevano qui, c'è un problema con HttpContext, attualmente Typemock è l'unica struttura che può fingere direttamente senza involucri o astrazioni.

1

Questo forse nella tua strada ... Phil Haack mostra, con l'aiuto di Rhino, come fingere httpcontext in asp mvc ma immagino che possa essere applicato alle webform.

Clicky!!

Spero che questo aiuti.

0

L'oggetto cache è difficile da deridere perché è un'area sigillata del framework .NET. Generalmente mi aggiro creando una classe wrapper cache che accetta un oggetto gestore cache. Per il test, utilizzo un gestore di cache fittizio; per la produzione, utilizzo un gestore cache che accede effettivamente a HttpRuntime.Cache.

Fondamentalmente, astraggo il cache personalmente.

16
HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 
0

Un esempio per coloro che utilizzano MVC 3 e MOQ:

Il mio metodo di controllo ha la seguente riga:

model.Initialize(HttpContext.Cache[Constants.C_CustomerTitleList] 
as Dictionary<int, string>); 

Come tale, qualsiasi unit test avrà esito negativo, come non sto impostando su HttpContext.Cache.

Nel mio test di unità, mi arrangio come segue:

HttpRuntime.Cache[Constants.C_CustomerTitleList] = new Dictionary<int, string>(); 

var mockRequest = new Mock<HttpRequestBase>(); 
mockRequest.SetupGet(m => m.Url).Returns(new Uri("http://localhost")); 

var context = new Mock<HttpContextBase>(MockBehavior.Strict); 
context.SetupGet(x => x.Request).Returns(mockRequest.Object); 
context.SetupGet(x => x.Cache).Returns(HttpRuntime.Cache); 

var controllerContext = new Mock<ControllerContext>(); 
controllerContext.SetupGet(x => x.HttpContext).Returns(context.Object); 

customerController.ControllerContext = controllerContext.Object; 
5

v'è un approccio più recente per contribuire ad affrontare specificamente Cache nel test di unità.

Si consiglia di utilizzare il nuovo approccio di Microsoft MemoryCache.Default. È necessario utilizzare .NET Framework 4.0 o versioni successive e includere un riferimento a System.Runtime.Caching.

See articolo qui ->http://msdn.microsoft.com/en-us/library/dd997357(v=vs.100).aspx

MemoryCache.Default funziona sia per le applicazioni web e non-web. Quindi l'idea è di aggiornare la tua webapp per rimuovere i riferimenti a HttpContext.Current.Cache e sostituirli con riferimenti a MemoryCache.Default. Successivamente, quando si esegue decidere di Unit Test questi stessi metodi, l'oggetto cache è ancora disponibile e non sarà null. (Perché non dipende da un HttpContext.)

In questo modo non è nemmeno necessario prendere in giro il componente cache.

1

se non si cura di cache di test si può fare qui di seguito:

[TestInitialize] 
    public void TestInit() 
    { 
     HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 
    } 

Inoltre è possibile moq come qui di seguito

var controllerContext = new Mock<ControllerContext>(); 
     controllerContext.SetupGet(p => p.HttpContext.Session["User"]).Returns(TestGetUser); 
     controllerContext.SetupGet(p => p.HttpContext.Request.Url).Returns(new Uri("http://web1.ml.loc")); 
0

potrebbe provare ...

Isolate.WhenCalled(() => HttpContext.Current).ReturnRecursiveFake(); 
var fakeSession = HttpContext.Current.Session; 
Isolate.WhenCalled(() => fakeSession.SessionID).WillReturn("1");