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

时间:2015-12-29 21:18:50

标签: c# asp.net asp.net-mvc unit-testing

------- 请参阅下面的更新,因为我现在已经设置了依赖注入和MOQ模拟框架的使用。我仍然希望拆分我的存储库,因此它不会直接依赖于在同一个函数中拉动windowsUser。

我在Intranet站点中有一个Web API,用于填充下拉列表。下拉列表后面的查询将windows用户名作为参数返回列表。

我意识到我没有正确设置所有这些因为我无法对其进行单元测试。我需要知道这"应该如何"设置为允许单元测试,然后单元测试应该是什么样的。

其他信息:这是一个ASP.NET MVC 5应用程序。

接口

public interface ITestRepository
{
    HttpResponseMessage DropDownList();
}

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

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

更新1

我根据BartoszKP的建议更新了我的控制器,以显示依赖注入。

更新控制器

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

更新2

我决定使用MOQ作为单元测试的模拟框架。我能够测试一些简单的东西,如下所示。 这会测试一个不带任何参数且不包含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);
}

我需要帮助测试DropDownList方法 (包含获取windowsUser的代码的方法) 。我需要建议如何将这种方法分开。我知道这两个部分不应该采用相同的方法。我不知道如何安排拆分windowsUser变量。我意识到这应该作为参数引入,但我无法弄清楚如何。

2 个答案:

答案 0 :(得分:2)

您通常不对单元测试存储库进行单元测试(集成测试验证它们是否真正将数据正确保存在数据库中) - 请参阅例如this article on MSDN

  

通常,很难对存储库本身进行单元测试,因此为它们编写集成测试通常会更好。

所以,让我们专注于只测试控制器。

更改控制器以在其构造函数中将IExampleRepository作为参数:

private IExampleRepository _exampleRepository;

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

然后,在单元测试中,使用一个模拟框架(例如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));
    }  
}

这只是一个快速而简单的示例 - CreateRepositoryStub方法当然应该提取到某个测试实用程序类。也许它应该返回一个流畅的界面,使测试的 Arrange 部分在给定的内容上更具可读性。更像是:

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

(当然,更好的名称反映了您的业务逻辑)。

对于ASP.NET MVC,框架需要知道如何构造控制器。幸运的是,ASP.NET支持依赖注入范例,using MVC unity时不需要无参数构造函数。

另请注意Richard Szalay的评论:

  

您不应在WebApi中使用HttpContext.Current - 您可以使用来自base.User且可模仿的HttpRequestBase.User。如果您确实想继续使用HttpContext.Current,请查看Mock HttpContext.Current in Test Init Method

答案 1 :(得分:0)

当我说代码访问一些我不能轻易参数化的全局静态或其他混乱的东西时尝试使旧代码可测试时,我觉得非常有用的一个技巧是在虚拟方法调用中包装对资源的访问。然后,您可以对正在测试的系统进行子类化,并在单元测试中使用它。

示例,在System.Random

中使用硬依赖项
public class Untestable
{
    public int CalculateSomethingRandom()
    {
        return new Random().Next() + new Random().Next();
    }
}

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

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

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

现在我们可以创建该类的可测试版本:

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;
    }
}

这种方法的缺点是你不能使用(大多数)隔离框架来实现受保护的方法(很容易),并且有充分的理由,因为受保护的方法是内部的,并且不应该对你的所有重要单元测试。它仍然是一种非常方便的方法,可以让测试成为可能,因此您可以重构它们,而不必花费10个小时而不进行测试,在您进入“安全”之前尝试对代码进行重大的架构更改。

要记住另一个工具,我发现它不时派上用场!

编辑:更具体地说,在您的情况下,您可能想要创建protected virtual string GetLoggedInUserName()。从技术上讲,这将保持对HttpContext.Current.User.Identity.Name的实际调用未经测试,但您将把它隔离到最简单的最小方法,这样您就可以测试代码使用正确的args调用正确的方法适当的次数,然后你只需要知道HttpContext.Current.User.Identity.Name包含你想要的东西。以后可以将其重构为某种用户管理器或登录用户提供程序,您将看到最适合的内容。