2012-12-14 51 views
2

我已經閱讀了更多關於單元測試的內容,並決定讓它工作。我挖掘出一個使用存儲庫模式,依賴注入和EF使用ASP.NET MVC編寫的項目。我的第一個任務是單元測試一個控制器。這裏是一個片段從控制器到測試:單元測試現有的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); 
    } 

我要測試的詳細方法,並具有以下查詢:

1)Globals.UserID屬性檢索從會話對象中的當前用戶。我如何輕鬆地測試這個(我使用內置的VS2010單元測試和Moq)

2)我在這裏調用AttachmentHelper.GetIconFromFileName(),它只是查看文件的擴展名並顯示一個圖標。我還在附件存儲庫中調用GetPersonAttachments,調用GetPerson,GetAddress和GetCountries以及調用創建的擴展方法將List轉換爲SelectListItem的IEnumerable。

這個控制器的行爲是一個壞習慣的例子嗎?它使用了大量的存儲庫和其他輔助方法。從我所看到的,單元測試這個單一動作將需要大量的代碼。這反效果嗎?

在一個測試項目中測試一個簡單的控制器的單元是一回事,但是當你進入像這樣的真實代碼時,它可能變成一個怪物。

我想我的問題真的是我應該重構我的代碼,以便更容易測試,或者我的測試變得更復雜,以滿足當前的代碼?

+0

你有沒有考慮過任何的Mapping框架,如Glue,AutoMapper,EmitMapper?對於這種特殊情況,我會盡量不採用單元測試,而是採用SpecFlow等功能測試。 – amdmax

+2

此外,單元測試應該在您編寫項目代碼*時完成*。整個觀點是測試有助於推動設計。試圖在事實失敗後嘗試應用測試。 –

回答

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

+1 ..雖然還是太大了。希望OP可以減少這一點。我**憎恨**看到這樣大的行動:( –

+0

+1)我認爲添加輔助方法可能是這裏的前進方向,很可能是我的行爲太大,反過來導致測試遠比複雜得多他們需要成爲。 – Paul

2

只是想詳細說明一下主題。我們試圖進行單元測試的是邏輯。沒有太多的控制器。所以在這種特殊情況下,我會做下一個:提取方法,它返回模型而不是視圖。將嘲笑的回購注入到控制器對象中。在行使之後,映射將確保所有屬性都充滿期望值。另一種方法是生成JSON並確保所有屬性都被適當地填充。但是,我會努力在映射部分本身上進行單元測試,然後將BDD用於集成測試。

1

我將所有的模型構造代碼移動到模型本身的構造函數中。我寧願保持僅限於極少數簡單的任務控制器:

  • 選擇合適的視圖(如果控制器動作允許多個視圖)
  • 選擇正確的視圖模型
  • 權限/安全
  • 查看模型驗證

因此,您的詳細信息控制器變得更加簡單和測試變得更加更加易於管理:

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

現在你的控制器很緊,可測試,讓我們來看看你的模型:

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

     ... public properties/methods ... 
    } 

再次,在模型中的代碼封裝,只需要特別擔心它的屬性。

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靜態道具。一般來說,如果可能的話,我會避免靜態數據,但如果這是一個遺留系統,您需要「對象化」以獲得可測試性。一個簡單的方法是將其隱藏在接口後面,像這樣...

interface IGlobalUserId 
{ 
    int GetIt(); 
} 

...並執行您使用靜態數據的實現。從現在開始,你可以注入這個接口來隱藏它是靜態數據的事實。

「AttachmentHelper」也一樣。將其隱藏在界面後面。一般來說,XXXHelpers應該有警鐘 - 我認爲這是一種標誌,它不應該放置在他們應該在的位置(一個對象的一部分),而是混合在一起的各種東西的混合。