什么是按以下方式可能出现的问题与单元测试ASP.NET MVC的代码?(What are the p

2019-06-24 01:06发布

我一直在寻找的路上单元测试是在完成了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");
    }
}

我想这将是集成测试,因为我用的是真实的数据库(我宁愿一个inmemory一个)。 我通过把数据在我的数据库开始我的测试(每个测试在事务中运行和测试完成时被回滚)。 然后,我打电话给我的控制器,我真的不关心它是如何从数据库中检索数据(通过存储库或服务类),只是模型被发送到视图必须有我放进数据库又名我的说法,记录。 这个方法的很酷的事情是,很多时候,我可以继续添加复杂的多层,而无需改变我的控制器的测试:

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

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

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

Answer 1:

嗯。 这里的几件事情配合。

首先,它看起来像你想测试一个控制器方法。 伟大的:)

因此,这意味着,任何控制器的需求,应该被嘲笑。 这是因为

  1. 你不想担心这种依赖性里面发生了什么。
  2. 您可以验证依赖被称为/执行。

好了,让我们看看你做了什么,我会看看我是否能重构它,使其更多的测试。

-REMEMBER-我测试的控制器的方法 ,而不是控制器方法调用/东东依靠。

因此,这意味着我不关心服务实例或存储库实例(您决定要跟踪的建筑曾经方式)。

注:我已经把事情变得简单,所以我剥夺很多废话了,等等。

接口

首先,我们需要存储库的接口。 这可以被实现为内存回购,实体框架回购,等等。你会,看到为什么很快。

public interface ILESRepository
{
    IQueryable<Person> GetAll();
}

调节器

在这里,我们使用的接口。 这意味着它真的很容易,真棒使用模拟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);
    }
}

单元测试

好了 - 这里的神奇钱拍的东西。 首先,我们创建了一些虚伪的人。 和我一起在这里工作......我会告诉你,我们用这个在转瞬间。 这只是你的一个无聊的,简单的列表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" },
        }
    }
}

现在我们有测试本身。 我使用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());
    }
}

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

首先,我安排我的废话。 我首先创建的模拟ILESRepository 。 然后我说:如果有谁调用GetAll<Person>()方法,以及..不-really-打一个数据库或文件或任何..只是返回的人的名单,这在创建FakePeople.GetSomeFakePeople()

因此,这是在控制器中会发生什么?

var model = _repository.GetAll<Person>()
                       .FirstOrDefault(x => x.Id == id);

首先,我们要求我们的模拟打GetAll<Person>()方法。 我只是“设置了”返回的人的名单..所以后来我们有3个名单Person对象。 接下来,我们再调用FirstOrDefault(...)这个名单3级Person的对象..它返回单个对象或为空,这取决于价值id是。

田田! 这就是钱拍:)

现在又回到了单元测试的其余部分。

我们Act ,然后我们Assert 。 没什么难有。 对于加分,我verify ,我们已经实际上被称为GetAll<Person>()方法,在模拟..控制器的内部Index的方法。 这是一个安全的通话,以确保我们的控制器逻辑(我们正在测试)做正确的。

有时候,你可能要检查坏情况下的,就像在坏数据传递的人。 这意味着你可能永远不会得到模拟的方法(这是正确的),所以你verify ,他们从来没有所谓。

好了 - 问题,类?



Answer 2:

即使你不打算嘲笑的接口,我强烈建议你通过创建构造函数中的对象不隐藏对象的实际依赖,你是打破了单一职责原则和你正在编写未测试的代码。

最重要的事情时要考虑写测试是:“没有万能钥匙编写测试”。 有很多的工具,有帮你写测试,但真正的工作应该放在编写测试代码 ,而不是试图破解我们现有的代码编写测试通常最终成为一个集成测试,而不是单元测试。

创建一个构造函数中的新对象是第一个大的信号,你的代码是不可测试之一。

这些链接帮助我,当我过渡到开始写测试,让我告诉你,你开始后,将会成为您的日常工作中很自然的一部分,你会爱上编写测试的好处,我不能想象自己很多没有测试编写代码了

干净的代码指南(谷歌使用): http://misko.hevery.com/code-reviewers-guide/

为了获得更多的信息,请阅读以下内容:

http://misko.hevery.com/2008/09/30/to-new-or-not-to-new/

并观看MISKO Hevery这个视频播客

http://www.youtube.com/watch?v=wEhu57pih5w&feature=player_embedded

编辑:

这从Martin Fowler的文章解释古典和Mockist TDD方式之间的区别

http://martinfowler.com/articles/mocksArentStubs.html

作为一个总结:

  • 经典TDD方法:这意味着没有用的,如Web服务或数据库外部服务的例外创建代用品或双打(嘲笑,存根,假人)来测试一切可以。 古典测试人员使用双打外部服务只

    • 优点:当你测试你实际上是在测试你的应用的布线逻辑和逻辑本身(而不是在隔离)
    • 缺点:如果出现错误,你将有可能看到上百次试验失败,这将是很难找到的代码负责
  • Mockist TDD方式:继Mockist办法将单独测试所有的代码,因为他们会为每一个依赖创建双打人

    • 优点:你是在隔离应用程序的每个部分的测试。 如果出现错误,你到底知道它在哪里发生,因为只是一些测试会失败,只有一个最理想
    • 缺点:那么你有你所有的依赖关系,这使得测试有点困难一倍,但你可以使用工具,如AutoFixture为依赖关系自动创建双打

这是关于写测试代码另一个大文章

http://www.loosecouplings.com/2011/01/how-to-write-testable-code-overview.html



Answer 3:

也有一些缺点。

首先,当你拥有依赖于外部组件(如活数据库)的测试,该测试已不再是真正的可预测性。 它可能会因任何原因-网络中断,已更改密码的数据库帐户,缺少某些DLL等,所以,当你的测试突然失败,你不能立即知道在哪里的缺陷 。 它是一个数据库的问题? 在你们班上有些棘手的错误吗?

当你能马上回答这个问题,只是知道哪些测试失败,你有令人羡慕的质量缺陷定位 。

其次,如果一个数据库的问题,所有的测试依赖于它就会失败一次。 这可能不是那么严重,因为你也许可以实现的原因是什么,但我保证它会让你放慢脚步,检查每个之一。 普遍的失败可能会掩盖真实的问题,因为你不想看每个50个测试除外。

我知道你想听到的,除了执行时间的因素,但无论那确实。 你要频繁运行测试成为可能,以及更长的运行时间表示不欢迎那。

我有两个项目:一个具有600个测试,在10秒内,一个与在运行50秒40+测试(这个项目实际上并与数据库对话,故意)运行。 我频繁地在开发运行更快的测试套件。 猜猜哪一个我觉得比较容易的工作?

所有这一切都表示,目前在测试的外部元件值。 只要是在你的单元测试。 集成测试更脆,更慢。 这使得他们更昂贵。



Answer 4:

在单元测试中访问数据库有以下后果:

  1. 性能 。 填充数据库和访问它是缓慢的。 在更多的测试,你有,等待的时间越长。 如果您使用嘲笑你的控制器测试可以在一对夫妇每毫秒的运行,如果它是直接使用数据库相比秒。
  2. 复杂性 。 对于共享数据库,你必须处理在多个代理商正在运行针对同一数据库测试并发问题。 数据库需要被提供,结构需要被创建,数据填充等,这变得相当复杂。
  3. 覆盖 。 你介意发现某些条件几乎是不可能的测试没有嘲笑。 实例可以包括验证该怎么办时,数据库超时。 或者做什么,如果发送电子邮件失败。
  4. 维护 。 更改数据库模式,特别是如果它频繁,会影响使用该数据库几乎每一个测试。 在刚开始的时候,你有10次测试中,它可能看起来不多,但是当你有2000米的测试考虑。 你也可能会发现不断变化的业务规则和适应的测试更加复杂,因为你必须修改填充数据库来验证业务规则的数据。

你要问它是否是值得的测试业务规则。 在大多数情况下,答案可能是“不”。

我采取的办法是:

  1. 单元类(控制器,服务层等)以嘲笑出的依赖关系和模拟,可能会发生(如数据库错误等)的条件。 这些测试验证业务逻辑和一个目标是获得的决策路径尽可能多的覆盖面。 使用类似的工具PEX突出你从来没有想过的任何问题。 你会惊奇地发现你的应用程序需要多少强大的(且有弹性的)是固定的一些问题PEX的亮点了。
  2. 写数据库测试,以验证ORM我使用与底层数据库的作品。 你会惊奇地发现问题EF和其他ORM有一定的数据库引擎(和版本)。 这些测试也用于调谐性能并减少被发送到和从数据库中查询和数据的量是有用的。
  3. 编写自动化浏览器和验证系统的实际工作编码的UI测试。 在这种情况下,我会用手填充数据库之前的一些数据。 这些测试自动化只是我会手动完成测试。 其目的是验证关键部分仍然有效。


文章来源: What are the possible problems with unit testing ASP.NET MVC code in the following way?