【问题标题】:Unit testing an existing ASP.NET MVC controller对现有 ASP.NET MVC 控制器进行单元测试
【发布时间】:2012-12-14 02:31:53
【问题描述】:

我阅读了越来越多有关单元测试的内容,并决心将其付诸实践。我使用存储库模式、依赖注入和 EF 挖出了一个用 ASP.NET MVC 编写的项目。我的第一个任务是对控制器进行单元测试。这是来自控制器的用于测试的 sn-p:

 IUserRepository _userRepository;
    IAttachmentRepository _attachmentRepository;
    IPeopleRepository _peopleRepository;
    ICountryRepository _countryRepository;

    public UserController(IUserRepository userRepo, IAttachmentRepository attachRepo, IPeopleRepository peopleRepo, ICountryRepository countryRepo)
    {
        _userRepository = userRepo;
        _attachmentRepository = attachRepo;
        _peopleRepository = peopleRepo;
        _countryRepository = countryRepo;
    }

    public ActionResult Details()
    {
        UserDetailsModel model = new UserDetailsModel();

        foreach (var doc in _attachmentRepository.GetPersonAttachments(Globals.UserID))
        {
            DocumentItemModel item = new DocumentItemModel();
            item.AttachmentID = doc.ID;
            item.DocumentIcon = AttachmentHelper.GetIconFromFileName(doc.StoragePath);
            item.DocumentName = doc.DocumentName;
            item.UploadedBy = string.Format("{0} {1}", doc.Forename, doc.Surname);
            item.Version = doc.VersionID;

            model.Documents.Add(item);
        }

        var person = _peopleRepository.GetPerson();
        var address = _peopleRepository.GetAddress();

        model.PersonModel.DateOfBirth = person.DateOfBirth;
        model.PersonModel.Forename = person.Forename;
        model.PersonModel.Surname = person.Surname;
        model.PersonModel.Title = person.Title;

        model.AddressModel.AddressLine1 = address.AddressLine1;
        model.AddressModel.AddressLine2 = address.AddressLine2;
        model.AddressModel.City = address.City;
        model.AddressModel.County = address.County;
        model.AddressModel.Postcode = address.Postcode;
        model.AddressModel.Telephone = address.Telephone;

        model.DocumentModel.EntityType = 1;
        model.DocumentModel.ID = Globals.UserID;
        model.DocumentModel.NewFile = true;

        var countries = _countryRepository.GetCountries();

        model.AddressModel.Countries = countries.ToSelectListItem(1, c => c.ID, c => c.CountryName, c => c.CountryName, c => c.ID.ToString());

        return View(model);
    }

我想测试 Details 方法并有以下查询:

1) Globals.UserID 属性从会话对象中检索当前用户。我怎样才能轻松测试这个(我正在使用内置的 VS2010 单元测试和起订量)

2) 我在这里调用 AttachmentHelper.GetIconFromFileName(),它只是查看文件的扩展名并显示一个图标。我还在附件存储库中调用 GetPersonAttachments,调用 GetPerson、GetAddress 和 GetCountries 以及调用创建的扩展方法将 List 转换为 SelectListItem 的 IEnumerable。

此控制器操作是否是不良做法的示例?它使用了大量的存储库和其他辅助方法。据我所知,对这个单一动作进行单元测试将需要大量代码。这会适得其反吗?

在测试项目中对一个简单的控制器进行单元测试是一回事,但是当你进入像这样的现实生活中的代码时,它可能会变成一个怪物。

我想我的问题真的是我应该重构我的代码以使其更易于测试,还是我的测试应该变得更加复杂以满足当前代码?

【问题讨论】:

  • 您是否考虑过任何映射框架,例如 Glue、AutoMapper、EmitMapper?对于这种特殊情况,我会尝试不采用单元测试,而是采用 SpecFlow 等功能测试。
  • 另外,单元测试应该在编写项目代码时进行。重点是测试有助于推动设计。在事后尝试应用测试错过了很多重点。

标签: c# asp.net-mvc visual-studio-2010 unit-testing tdd


【解决方案1】:

复杂的测试与复杂的代码一样糟糕:它们容易出现错误。因此,为了使您的测试保持简单,重构您的应用程序代码以使其更易于测试通常是一个好主意。例如,您应该将您的 Details() 方法中的映射代码提取到单独的辅助方法中。然后,您可以非常轻松地测试这些方法,而不必担心测试 Details() 的所有疯狂组合。

我已经提取了下面的人员和地址映射部分,但您可以将其分开更多。我只是想让你明白我的意思。

    public ActionResult Details() {
        UserDetailsModel model = new UserDetailsModel();

        foreach( var doc in _attachmentRepository.GetPersonAttachments( Globals.UserID ) ) {
            DocumentItemModel item = new DocumentItemModel();
            item.AttachmentID = doc.ID;
            item.DocumentIcon = AttachmentHelper.GetIconFromFileName( doc.StoragePath );
            item.DocumentName = doc.DocumentName;
            item.UploadedBy = string.Format( "{0} {1}", doc.Forename, doc.Surname );
            item.Version = doc.VersionID;

            model.Documents.Add( item );
        }

        var person = _peopleRepository.GetPerson();
        var address = _peopleRepository.GetAddress();

        MapPersonToModel( model, person );

        MapAddressToModel( model, address );

        model.DocumentModel.EntityType = 1;
        model.DocumentModel.ID = Globals.UserID;
        model.DocumentModel.NewFile = true;

        var countries = _countryRepository.GetCountries();

        model.AddressModel.Countries = countries.ToSelectListItem( 1, c => c.ID, c => c.CountryName, c => c.CountryName, c => c.ID.ToString() );

        return View( model );
    }

    public void MapAddressToModel( UserDetailsModel model, Address address ) {
        model.AddressModel.AddressLine1 = address.AddressLine1;
        model.AddressModel.AddressLine2 = address.AddressLine2;
        model.AddressModel.City = address.City;
        model.AddressModel.County = address.County;
        model.AddressModel.Postcode = address.Postcode;
        model.AddressModel.Telephone = address.Telephone;
    }

    public void MapPersonToModel( UserDetailsModel model, Person person ) {
        model.PersonModel.DateOfBirth = person.DateOfBirth;
        model.PersonModel.Forename = person.Forename;
        model.PersonModel.Surname = person.Surname;
        model.PersonModel.Title = person.Title;
    }

【讨论】:

  • +1.. 不过还是太大了。希望OP可以减少这种情况。我讨厌看到这么大的动作:(
  • +1 我认为添加辅助方法可能是前进的方向。很可能是我的操作太大,这反过来又导致测试比它们需要的复杂得多。
【解决方案2】:

只是想详细说明一下主题。我们试图单元测试的是逻辑。在控制器中没有太多。因此,在这种特殊情况下,我将执行下一个:返回模型而不是视图的提取方法。将模拟的 repos 注入控制器对象。并且在执行映射后将确保所有属性都填充有预期值。另一种方法是生成 JSON 并确保正确填充所有属性。但是,我会努力对映射部分本身进行单元测试,然后考虑将 BDD 用于集成测试。

【讨论】:

    【解决方案3】:

    我会将您的所有模型构造代码移动到模型本身的构造函数中。我更喜欢将控制器限制在少数几个简单的任务上:

    • 选择正确的视图(如果控制器操作允许多个视图)
    • 选择正确的视图模型
    • 权限/安全
    • 查看模型验证

    因此,您的 Details 控制器变得更加简单,测试变得更易于管理:

    public ActionResult Details() {
        return View(new UserDetailsModel(Globals.UserId);
    }
    

    既然您的控制器是紧凑且可测试的,让我们看看您的模型:

        public class UserDetailsModel {
            public UserDetailsModel(int userId) {
               ... instantiation of properties goes here...
             }
    
            ... public properties/methods ...
        }
    

    同样,模型中的代码已被封装,只需要特别关注它的属性。

    【讨论】:

      【解决方案4】:

      正如@KevinM1 已经提到的,如果您正在练习 TDD(您的问题中有该标签),那么您正在编写测试在实现之前

      您首先为控制器的 Detail 方法编写一个测试。编写此测试时,您注意到需要将人员映射到 UserDetailsModel。编写测试时,您“隐藏了复杂性”,这些复杂性不属于您要在抽象后面测试的内容的实际实现。在这种情况下,您可能会创建一个 IUserDetailModelMapper。编写第一个测试时,您可以通过创建控制器将其变为绿色。

      public class UserController
      {
         ctor(IUserRepository userRepo, IUserDetailModelMapper mapper){...}
      
         public ActionResult Details()
         {
            var model = _mapper.Map(_userRepo.GetPerson());
            return View(model);
         }
      }
      

      当您稍后为您的映射器编写测试时,您说您需要使用一些名为 Globals.UserId 的静态属性。一般来说,如果可能的话,我会避免使用静态数据,但如果这是一个遗留系统,你需要“客观化”它以使我可以测试。一种简单的方法是将其隐藏在界面后面,就像这样......

      interface IGlobalUserId
      {
        int GetIt();
      }
      

      ...并执行一个使用静态数据的实现。从现在开始,你可以注入这个接口来隐藏它是静态数据的事实。

      “AttachmentHelper”也是如此。将其隐藏在界面后面。不过,一般来说,XXXHelpers 应该敲响警钟 - 我会说这表明没有将方法放置在它们应该放置的位置(对象的一部分),而是将各种混合在一起的东西混合在一起。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-14
        相关资源
        最近更新 更多