简体   繁体   English

对使用Windows身份验证的控制器进行单元测试

[英]Unit Testing a controller that uses windows authentication

------- Please see updates below as I now have this set up for dependency injection and the use of the MOQ mocking framework. ------- 请参阅下面的更新,因为我现在已经为依赖注入和MOQ模拟框架的使用设置了此设置。 I'd still like to split up my repository so it doesn't directly depend on pulling the windowsUser within the same function. 我仍然想拆分我的存储库,因此它不直接依赖于在同一函数中提取windowsUser。


I have a Web API in an intranet site that populates a dropdown. 我在填充下拉列表的Intranet站点中有一个Web API。 The query behind the dropdown takes the windows username as a parameter to return the list. 下拉菜单后面的查询将Windows用户名作为参数返回列表。

I realize I don't have all of this set up correctly because I'm not able to unit test it. 我意识到我没有正确设置所有这些设置,因为我无法对其进行单元测试。 I need to know how this "should" be set up to allow unit testing and then what the unit tests should look like. 我需要知道如何“设置”此功能以允许进行单元测试,然后知道单元测试的外观。

Additional info: this is an ASP.NET MVC 5 application. 其他信息:这是一个ASP.NET MVC 5应用程序。

INTERFACE 接口

public interface ITestRepository
{
    HttpResponseMessage DropDownList();
}

REPOSITORY REPOSITORY

public class ExampleRepository : IExampleRepository
{
    //Accessing the data through Entity Framework
    private MyDatabaseEntities db = new MyDatabaseEntities();

    public HttpResponseMessage DropDownList()
    {
        //Get the current windows user
        string windowsUser =  HttpContext.Current.User.Identity.Name;

        //Pass the parameter to a procedure running a select query
        var sourceQuery = (from p in db.spDropDownList(windowsUser)
                           select p).ToList();

        string result = JsonConvert.SerializeObject(sourceQuery);
        var response = new HttpResponseMessage();
        response.Content = new StringContent(result, System.Text.Encoding.Unicode, "application/json");

        return response;            
    }
}

CONTROLLER CONTROLLER

public class ExampleController : ApiController
{
    private IExampleRepository _exampleRepository;

    public ExampleController()
    {
        _exampleRepository = new ExampleRepository();
    }

    [HttpGet]
    public HttpResponseMessage DropDownList()
    {
        try
        {
            return _exampleRepository.DropDownList();
        }
        catch
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
        }
    }
}

UPDATE 1 更新1

I have updated my Controller based on BartoszKP's suggestion to show dependency injection. 我已根据BartoszKP的建议更新了我的控制器,以显示依赖项注入。

UPDATED CONTROLLER 更新的控制器

public class ExampleController : ApiController
{
    private IExampleRepository _exampleRepository;

    //Dependency Injection
    public ExampleController(IExampleRepository exampleRepository)
    {
        _exampleRepository = exampleRepository;
    }

    [HttpGet]
    public HttpResponseMessage DropDownList()
    {
        try
        {
            return _exampleRepository.DropDownList();
        }
        catch
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
        }
    }
}

UPDATE 2 更新2

I have decided to use MOQ as a mocking framework for unit testing. 我决定将MOQ用作单元测试的模拟框架。 I'm able to test something simple, like the following. 我可以测试一些简单的东西,如下所示。 This would test a simple method that doesn't take any parameters and doesn't include the windowsUser part . 这将测试一个不带任何参数并且不包含windowsUser部分的简单方法

[TestMethod]
public void ExampleOfAnotherTest()
{
    //Arrange
    var mockRepository = new Mock<IExampleRepository>();
    mockRepository
        .Setup(x => x.DropDownList())
        .Returns(new HttpResponseMessage(HttpStatusCode.OK));

    ExampleController controller = new ExampleController(mockRepository.Object);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    //Act            
    var response = controller.DropDownList();

    //Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}

I need help testing the DropDownList method (one that does include code to get the windowsUser) . 我需要测试DropDownList方法的帮助该方法确实包含获取windowsUser的代码) I need advice on how to break this method apart. 我需要有关如何分开此方法的建议。 I know both parts shouldn't been in the same method. 我知道这两个部分不应该使用相同的方法。 I don't know how to arrange splitting out the windowsUser variable. 我不知道如何安排拆分出windowsUser变量。 I realize this really should be brought in as a parameter, but I can't figure out how. 我意识到这确实应该作为参数引入,但是我不知道怎么做。

You usually do not unit-test repositories (integration tests verify if they really persist the data in the database correctly) - see for example this article on MSDN : 您通常不对存储库进行单元测试(集成测试会验证它们是否确实将数据正确地持久存储在数据库中)-例如,请参见MSDN上的本文

Typically, it is difficult to unit test the repositories themselves, so it is often better to write integration tests for them. 通常,很难对存储库本身进行单元测试,因此通常为它们编写集成测试会更好。

So, let's focus on testing only the controller. 因此,让我们专注于仅测试控制器。

Change the controller to take IExampleRepository in its constructor as a parameter: 更改控制器以使其构造函数中的IExampleRepository作为参数:

private IExampleRepository _exampleRepository;

public ExampleController(IExampleRepository exampleRepository)
{
    _exampleRepository = exampleRepository;
}

Then, in your unit tests, use one of mocking frameworks (such as RhinoMock for example) to create a stub for the sole purpose of testing the controller. 然后,在单元测试中,仅出于测试控制器的目的,请使用其中一种模拟框架(例如RhinoMock )创建存根。

[TestFixture]
public class ExampleTestFixture
{
    private IExampleRepository CreateRepositoryStub(fake data)
    {
        var exampleRepositoryStub = ...; // create the stub with a mocking framework

        // make the stub return given fake data

        return exampleRepositoryStub;
    }

    [Test]
    public void GivenX_WhenDropDownListIsRequested_ReturnsY()
    {
        // Arrange
        var exampleRepositoryStub = CreateRepositoryStub(X);
        var exampleController = new ExampleController(exampleRepositoryStub);

        // Act
        var result = exampleController.DropDownList();

        // Assert
        Assert.That(result, Is.Equal(Y));
    }  
}

This is just a quick&dirty example - CreateRepositoryStub method should be of course extracted to some test utility class. 这只是一个简单而肮脏的示例-当然应该将CreateRepositoryStub方法提取到某些测试实用程序类中。 Perhaps it should return a fluent interface to make the test's Arrange section more readable on what is given. 也许它应该返回一个流畅的界面,以使测试的“ 安排”部分对给出的内容更具可读性。 Something more like: 更像是:

// Arrange
var exampleController
    = GivenAController()
      .WithFakeData(X);

(with better names that reflect your business logic of course). (更好的名称当然可以反映您的业务逻辑)。


In case of ASP.NET MVC, the framework needs to know how to construct the controller. 对于ASP.NET MVC,框架需要知道如何构造控制器。 Fortunately, ASP.NET supports the Dependency Injection paradigm and a parameterless constructor is not required when using MVC unity . 幸运的是,ASP.NET支持依赖注入范例, 使用MVC unity时不需要无参数构造函数。


Also, note the comment by Richard Szalay : 另外,请注意Richard Szalay的评论:

You shouldn't use HttpContext.Current in WebApi - you can use base.User which comes from HttpRequestBase.User and is mockable. 您不应该在WebApi中使用HttpContext.Current可以使用base.User ,它来自HttpRequestBase.User并且是可base.User If you really want to continue using HttpContext.Current , take a look at Mock HttpContext.Current in Test Init Method 如果您真的想继续使用HttpContext.Current ,请查看Test Init方法中的Mock HttpContext.Current。

One trick that I find very useful when trying to make old code testable when said code is accessing some global static or other messy stuff that I can't easily just parameterize is to wrap access to the resource in a virtual method call. 当上述代码访问一些我无法轻易参数化的全局静态或其他混乱的东西时,尝试使旧代码可测试时,我发现非常有用的一个技巧是将对资源的访问包装在虚拟方法调用中。 Then you can subclass your system under test and use that in the unit test instead. 然后,您可以将被测系统子类化,并在单元测试中使用它。

Example, using a hard dependency in the System.Random class 示例,在System.Random类中使用硬依赖性

public class Untestable
{
    public int CalculateSomethingRandom()
    {
        return new Random().Next() + new Random().Next();
    }
}

Now we replace var rng = new Random(); 现在我们替换var rng = new Random();

public class Untestable
{
    public int CalculateSomethingRandom()
    {
        return GetRandomNumber() + GetRandomNumber();
    }

    protected virtual int GetRandomNumber()
    {
        return new Random().Next();
    }
}

Now we can create a testable version of the class: 现在我们可以创建该类的可测试版本:

public class Testable : Untestable
{
    protected override int GetRandomNumber()
    {
        // You can return whatever you want for your test here,
        // it depends on what type of behaviour you are faking.
        // You can easily inject values here via a constructor or
        // some public field in the subclass. You can also add
        // counters for times method was called, save the args etc.
        return 4;
    }
}

The drawback with this method is that you can't use (most) isolation frameworks to implement protected methods (easily), and for good reason, since protected methods are sort of internal and shouldn't be all that important to your unit tests. 这种方法的缺点是您不能(轻松地)使用(大多数)隔离框架来实现受保护的方法,并且有充分的理由,因为受保护的方法是内部的,因此对于单元测试而言并不那么重要。 It's still a really handy way of getting things covered with tests so you can refactor them, instead of having to spend 10 hours without tests, trying to do major architectual changes to your code before you get to "safety". 这仍然是使测试涵盖所有内容的一种非常便捷的方法,因此您可以重构它们,而不必花10个小时不进行测试,而是在获得“安全性”之前尝试对代码进行重大的体系结构更改。

Just another tool to keep in mind, I find it comes in handy from time to time! 只是要记住的另一个工具,我发现它有时会派上用场!

EDIT: More concretely, in your case you might want to create a protected virtual string GetLoggedInUserName() . 编辑:更具体地讲,在您的情况下,您可能想创建一个protected virtual string GetLoggedInUserName() This will technically speaking keep the actual call to HttpContext.Current.User.Identity.Name untested, but you will have isolated it to the simplest smallest possible method, so you can test that the code is calling the correct method the right amount of times with the correct args, and then you simply have to know that HttpContext.Current.User.Identity.Name contains what you want. 从技术上讲,这将使未经测试的HttpContext.Current.User.Identity.Name实际调用保持未经测试,但您已将其隔离到了最简单的最小方法,因此您可以测试代码是否在正确的时间调用了正确的方法。与正确的参数,然后您只需要知道HttpContext.Current.User.Identity.Name包含所需的内容。 This can later be refactored into some sort of user manager or logged in user provider, you'll see what suits best as you go along. 以后可以将其重构为某种类型的用户管理器或登录用户提供程序,然后您会发现最适合的方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM