以下列方式对ASP.NET MVC代码进行单元测试可能存在哪些问题?

ena*_*rik 4 unit-testing asp.net-mvc-3

我一直在关注NuGetGallery中单元测试的方式.我观察到,当测试控制器时,服务类被模拟.这对我来说很有意义,因为在测试控制器逻辑时,我不想担心下面的架构层.在使用这种方法一段时间之后,我注意到当我的服务类发生变化时,我经常在我的控制器测试中修复我的模拟.为了解决这个问题,在没有咨询比我更聪明的人的情况下,我开始编写这样的测试(别担心,我没有那么远):

public class PersonController : Controller
{
    private readonly LESRepository _repository;

    public PersonController(LESRepository repository)
    {
        _repository = repository;
    }

    public ActionResult Index(int id)
    {
        var model = _repository.GetAll<Person>()
            .FirstOrDefault(x => x.Id == id);

        var viewModel = new VMPerson(model);
        return View(viewModel);
    }
}

public class PersonControllerTests
{
    public void can_get_person()
    {
        var person = _helper.CreatePerson(username: "John");
        var controller = new PersonController(_repository);
        controller.FakeOutContext();

        var result = (ViewResult)controller.Index(person.Id);
        var model = (VMPerson)result.Model;
        Assert.IsTrue(model.Person.Username == "John");
    }
}
Run Code Online (Sandbox Code Playgroud)

我想这将是集成测试,因为我使用的是真正的数据库(我更喜欢内存数据库).我通过将数据放入我的数据库开始我的测试(每个测试在一个事务中运行,并在测试完成时回滚).然后我调用我的控制器,我真的不在乎它如何从数据库中检索数据(通过存储库或服务类)只是要发送到视图的模型必须有我放入数据库的记录,也就是我的断言.关于这种方法的一个很酷的事情是,很多时候我可以继续添加更多层的复杂性,而无需更改我的控制器测试:

public class PersonController : Controller
{
    private readonly LESRepository _repository;
    private readonly PersonService _personService;

    public PersonController(LESRepository repository)
    {
        _repository = repository;
        _personService = new PersonService(_repository);
    }

    public ActionResult Index(int id)
    {
        var model = _personService.GetActivePerson(id);
        if(model  == null)
          return PersonNotFoundResult();

        var viewModel = new VMPerson(model);
        return View(viewModel);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我意识到我没有为PersonService创建一个接口并将其传递给我的控制器的构造函数.原因是1)我不打算模拟我的PersonService和2)我不觉得我需要注入我的依赖,因为我的PersonController现在只需要依赖一种类型的PersonService.

我是单位测试的新手,我总是很高兴被证明我错了.请指出为什么我测试我的控制器的方式可能是一个非常糟糕的主意(除了我的测试运行时间明显增加).

Pur*_*ome 5

嗯.这里有几件事情.

首先,看起来您正在尝试测试控制器方法.太棒了:)

所以这意味着,控制器需要的任何东西都应该被嘲笑.这是因为

  1. 您不想担心该依赖项内发生的事情.
  2. 您可以验证是否已调用/执行依赖项.

好吧,让我们来看看你做了什么,我会看看我是否可以重构它以使其更加可测试.

-EMEMBER-我正在测试CONTROLLER METHOD,而不是控制器方法调用/依赖的东西.

所以这意味着我不关心服务实例或存储库实例(您决定遵循的架构方式).

注意:我保持简单,所以我已经删除了很多废话,等等.

接口

首先,我们需要一个存储库接口.这可以实现为内存中的repo,实体框架repo等.你很快就会明白为什么.

public interface ILESRepository
{
    IQueryable<Person> GetAll();
}
Run Code Online (Sandbox Code Playgroud)

调节器

在这里,我们使用界面.这意味着使用模拟IRepository或真实实例非常简单和棒极了.

public class PersonController : Controller
{
    private readonly ILESRepository _repository;

    public PersonController(ILESRepository repository)
    {
       if (repository == null)
       {
           throw new ArgumentNullException("repository");
       }
        _repository = repository;
    }

    public ActionResult Index(int id)
    {
        var model = _repository.GetAll<Person>()
            .FirstOrDefault(x => x.Id == id);

        var viewModel = new VMPerson(model);
        return View(viewModel);
    }
}
Run Code Online (Sandbox Code Playgroud)

单元测试

好的 - 这是神奇的金钱拍摄的东西.首先,我们创造了一些假人.在这里和我一起工作......我会告诉你我们在哪里使用它.这只是一个无聊,简单的你POCO的清单.

public static class FakePeople()
{
    public static IList<Person> GetSomeFakePeople()
    {
        return new List<Person>
        {
            new Person { Id = 1, Name = "John" },
            new Person { Id = 2, Name = "Fred" },
            new Person { Id = 3, Name = "Sally" },
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们进行了测试.我正在使用xUnit我的测试框架和moq我的模拟.这里有任何框架都可以.

public class PersonControllerTests
{
    [Fact]
    public void GivenAListOfPeople_Index_Returns1Person()
    {
        // Arrange.
        var mockRepository = new Mock<ILESRepository>();
        mockRepository.Setup(x => x.GetAll<Person>())
                                   .Returns(
                                FakePeople.GetSomeFakePeople()
                                          .AsQueryable);
        var controller = new PersonController(mockRepository);
        controller.FakeOutContext();

        // Act.
        var result = controller.Index(person.Id) as ViewResult;

        // Assert.
        Assert.NotNull(result);
        var model = result.Model as VMPerson;
        Assert.NotNull(model);
        Assert.Equal(1, model.Person.Id);
        Assert.Equal("John", model.Person.Username);

        // Make sure we actually called the GetAll<Person>() method on our mock.
        mockRepository.Verify(x => x.GetAll<Person>(), Times.Once());
    }
}
Run Code Online (Sandbox Code Playgroud)

好吧,让我们来看看我做了什么.

首先,我安排我的废话.我先创建一个模拟的ILESRepository.然后我说:如果有人曾经调用过这个GetAll<Person>()方法,那么......不要 - 真正地击中数据库或文件或其他任何东西......只需返回创建的人员列表FakePeople.GetSomeFakePeople().

所以这就是控制器会发生的事情......

var model = _repository.GetAll<Person>()
                       .FirstOrDefault(x => x.Id == id);
Run Code Online (Sandbox Code Playgroud)

首先,我们要求我们的模拟命中GetAll<Person>()方法.我只是'设置'来返回一个人的列表..所以我们有一个3个Person对象的列表.接下来,我们FirstOrDefault(...)在这个包含3个Person对象的列表上调用a ..它返回单个对象或null,具体取决于它的值id.

田田!这是钱拍:)

现在回到单元测试的其余部分.

我们Act,然后我们Assert.那里没什么难的.对于奖励积分,我verify实际上已经在模拟器中调用了GetAll<Person>()方法,在Controller的Index方法中.这是一个安全调用,以确保我们的控制器逻辑(我们正在测试)是正确的.

有时,您可能希望检查错误的情况,例如传递错误数据的人.这意味着你可能永远不会得到模拟方法(这是正确的),所以你verify永远不会被调用.

好的 - 问题,课程?