2

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

 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。

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

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

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

4

4 回答 4

3

复杂的测试和复杂的代码一样糟糕:它们容易出现错误。因此,为了使您的测试保持简单,重构您的应用程序代码以使其更易于测试通常是一个好主意。例如,您应该将您的 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;
    }
于 2012-12-14T03:13:32.810 回答
2

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

于 2012-12-14T03:04:01.433 回答
1

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

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

因此,您的 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 ...
    }

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

于 2012-12-14T17:39:06.837 回答
0

正如@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 的静态道具。一般来说,如果可能的话,我会避免使用静态数据,但如果这是一个遗留系统,你需要“客观化”它以使 i 可测试。一种简单的方法是将其隐藏在界面后面,就像这样......

interface IGlobalUserId
{
  int GetIt();
}

...并在使用静态数据的地方进行实施。从现在开始,您可以注入此接口来隐藏它是静态数据的事实。

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

于 2013-02-08T16:56:13.250 回答