Pregunta

Considere un método en un ensamblado .NET:

public static string GetSecurityContextUserName()
{             
 //extract the username from request              
 string sUser = HttpContext.Current.User.Identity.Name;
 //everything after the domain     
 sUser = sUser.Substring(sUser.IndexOf("\\") + 1).ToLower();

 return sUser;      
}

Me gustaría llamar a este método desde una prueba unitaria utilizando el marco Moq. Este ensamblaje es parte de una solución de formularios web. La prueba de la unidad se ve así, pero me falta el código Moq.

//arrange 
 string ADAccount = "BUGSBUNNY";
 string fullADName = "LOONEYTUNES\BUGSBUNNY"; 

 //act    
 //need to mock up the HttpContext here somehow -- using Moq.
 string foundUserName = MyIdentityBL.GetSecurityContextUserName();

 //assert
 Assert.AreEqual(foundUserName, ADAccount, true, "Should have been the same User Identity.");

Pregunta :

  • ¿Cómo puedo usar Moq para organizar un objeto HttpContext falso con algún valor como 'MyDomain \ MyUser'?
  • ¿Cómo asocio esa falsificación con mi llamada en mi método estático en MyIdentityBL.GetSecurityContextUserName () ?
  • ¿Tiene alguna sugerencia sobre cómo mejorar este código / arquitectura?
¿Fue útil?

Solución

Webforms es notoriamente inestable por esta razón exacta: una gran cantidad de código puede depender de clases estáticas en la tubería asp.net.

Para probar esto con Moq, debe refactorizar su método GetSecurityContextUserName () para usar la inyección de dependencia con un objeto HttpContextBase .

HttpContextWrapper reside en System.Web.Abstraction , que se entrega con .Net 3.5. Es un contenedor para la clase HttpContext , y extiende HttpContextBase , y puede construir un HttpContextWrapper así:

var wrapper = new HttpContextWrapper(HttpContext.Current);

Aún mejor, puede burlarse de un HttpContextBase y configurar sus expectativas con Moq. Incluyendo el usuario conectado, etc.

var mockContext = new Mock<HttpContextBase>();

Con esto en su lugar, puede llamar a GetSecurityContextUserName (mockContext.Object) , y su aplicación está mucho menos acoplada al HttpContext estático de WebForms. Si va a realizar muchas pruebas que se basan en un contexto simulado, le recomiendo tomar un vistazo a la clase MvcMockHelpers de Scott Hanselman , que tiene una versión para usar con Moq. Maneja convenientemente gran parte de la configuración necesaria. Y a pesar del nombre, no necesita hacerlo con MVC: lo uso con éxito con aplicaciones de formularios web cuando puedo refactorizarlas para usar HttpContextBase .

Otros consejos

En general, para las pruebas unitarias ASP.NET, en lugar de acceder a HttpContext.Current, debe tener una propiedad de tipo HttpContextBase cuyo valor se establece mediante inyección de dependencia (como en la respuesta proporcionada por Womp).

Sin embargo, para probar funciones relacionadas con la seguridad, recomendaría usar Thread.CurrentThread.Principal (en lugar de HttpContext.Current.User). El uso de Thread.CurrentThread tiene la ventaja de ser reutilizable también fuera de un contexto web (y funciona igual en un contexto web porque el marco ASP.NET siempre establece ambos valores de la misma manera).

Para luego probar Thread.CurrentThread.Principal, generalmente uso una clase de ámbito que establece Thread.CurrentThread en un valor de prueba y luego se restablece al desechar:

using (new UserResetScope("LOONEYTUNES\BUGSBUNNY")) {
    // Put test here -- CurrentThread.Principal is reset when PrincipalScope is disposed
}

Esto encaja bien con el componente de seguridad .NET estándar, donde un componente tiene una interfaz conocida (IPrincipal) y una ubicación (Thread.CurrentThread.Principal), y funcionará con cualquier código que use / verifique correctamente con Thread. CurrentThread.Principal.

Una clase de alcance base sería algo como lo siguiente (ajuste según sea necesario para cosas como agregar roles):

class UserResetScope : IDisposable {
    private IPrincipal originalUser;
    public UserResetScope(string newUserName) {
        originalUser = Thread.CurrentPrincipal;
        var newUser = new GenericPrincipal(new GenericIdentity(newUserName), new string[0]);
        Thread.CurrentPrincipal = newUser;
    }
    public IPrincipal OriginalUser { get { return this.originalUser; } }
    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            Thread.CurrentPrincipal = originalUser;
        }
    }
}

Otra alternativa es, en lugar de usar la ubicación estándar del componente de seguridad, escribir su aplicación para usar los detalles de seguridad inyectados, p. agregue una propiedad ISecurityContext con un método GetCurrentUser () o similar, y luego utilícelo consistentemente en toda su aplicación, pero si va a hacer esto en el contexto de una aplicación web, entonces también podría usar el contexto inyectado preconstruido , HttpContextBase.

Echa un vistazo a esto http : //haacked.com/archive/2007/06/19/unit-tests-web-code-without-a-web-server-using-httpsimulator.aspx

Usando la clase httpSimulator, podrá pasar un HttpContext al controlador

HttpSimulator sim = new HttpSimulator("/", @"C:\intepub\?")
.SimulateRequest(new Uri("http://localhost:54331/FileHandler.ashx?
ticket=" + myticket + "&fileName=" + path));

FileHandler fh = new FileHandler();
fh.ProcessRequest(HttpContext.Current);

HttpSimulator implementa lo que necesitamos para obtener una instancia de HttpContext. Entonces no necesitas usar Moq aquí.

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

También puedes hacer moq como a continuación

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"));

Si está utilizando el modelo de seguridad CLR (como lo hacemos nosotros), entonces necesitará usar algunas funciones abstractas para obtener y establecer el principal actual si desea permitir la prueba, y usarlas cada vez que obtenga o configure el principal . Hacer esto le permite obtener / establecer el principal donde sea relevante (generalmente en HttpContext en la web, y en el hilo actual en otros lugares como pruebas unitarias). Esto se vería así:

public static IPrincipal GetCurrentPrincipal()
{
    return HttpContext.Current != null ?
        HttpContext.Current.User :
        Thread.CurrentThread.Principal;
}

public static void SetCurrentPrincipal(IPrincipal principal)
{
     if (HttpContext.Current != null) HttpContext.Current.User = principal'
     Thread.CurrentThread.Principal = principal;
}

Si usa un principal personalizado, estos se pueden integrar bastante bien en su interfaz, por ejemplo, Current llamaría a GetCurrentPrincipal y SetAsCurrent llamaría a SetCurrentPrincipal .

public class MyCustomPrincipal : IPrincipal
{
    public MyCustomPrincipal Current { get; }
    public bool HasCurrent { get; }
    public void SetAsCurrent();
}

Esto no está realmente relacionado con el uso de Moq para pruebas unitarias de lo que necesita.

Generalmente, en el trabajo tenemos una arquitectura en capas, donde el código en la capa de presentación es realmente solo para organizar las cosas que se muestran en la interfaz de usuario. Este tipo de código no está cubierto con pruebas unitarias. Todo el resto de la lógica reside en la capa empresarial, que no tiene que depender de la capa de presentación (es decir, referencias específicas de la interfaz de usuario, como HttpContext), ya que la interfaz de usuario también puede ser una aplicación WinForms y no necesariamente una aplicación web .

De esta forma, puede evitar perder el tiempo con los marcos Mock, intentando simular HttpRequests, etc., aunque a menudo puede ser necesario.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top