文章内容

2017/4/27 13:40:35,作 者: 黄兵

将 TDD 和 ASP.NET MVC 结合使用

使用单元测试创建 MVC 应用程序

  1. 在 Visual Studio 中,在“文件”菜单上,单击“新建项目”。

  2. 在“新建项目”对话框中的“已安装的模板”下,打开“Visual C#”或“Visual Basic”节点,然后选择“Web”。

  3. 选择“ASP.NET MVC Web 应用程序”模板。

  4. 将解决方案命名为 MvcContacts

  5. 单击“确定”

  6. 当显示“创建单元测试项目”对话框时,确保选择“是,创建单元测试项目”,然后单击“确定”。

    Visual Studio 将创建包含两个项目的解决方案,一个命名为 MvcContacts,另一个命名为 MvcContacts.Tests

  7. 在“测试”菜单上,单击“运行”,然后单击“解决方案中的所有测试”。

    结果将显示在“测试结果”窗口中。 测试通过。

  8. 在 MvcContacts.Tests 项目中,打开并检查帐户控制器测试类 (MvcContacts\MvcContacts.Tests\Controllers\AccountControllerTest) 和帐户控制器模型类 (MvcContacts\Models\AccountModels)。

    这些类详细介绍了如何创建 mock 接口和 TDD。 模拟是为类中的依赖项创建简单替代 (mock) 对象,以便在没有依赖项的情况下测试类的过程。若要测试接口,通常可创建一个用于实现要测试的接口的 mock 类。 例如,帐户控制器测试类中的 MockMembershipService 类实现 IMembershipService 接口,以模拟属于成员资格类的成员,如 ValidateUserCreateUser 和 ChangePassword 方法。 通过MockMembershipService 类,可测试用于创建用户帐户的操作方法、验证用户注册信息并更改用户的密码,而不必实例化成员资格类,如Membership

    在本节中,您将添加联系人元数据。 单元测试中将不使用联系人类元数据。 但由于它提供自动化的客户端和服务器端数据验证,因此会使示例更完整。

    添加模型元数据

    1. 在 MvcContacts\Models 文件夹中,创建一个名为 ContactMD 的新类文件。

      在此文件中,您将添加一个类 (ContactMD),该类将包含用于此演练的数据模型中的 Contact 实体对象的元数据。

    2. 用下面的代码替换文件中的代码:

    using System.ComponentModel.DataAnnotations;
    
    namespace MvcContacts.Models {
        [MetadataType(typeof(ContactMD))]
        public partial class Contact {
            public class ContactMD {
                [ScaffoldColumn(false)]
                public object Id { get; set; }
                [Required()]
                public object FirstName { get; set; }
                [Required()]
                public object LastName { get; set; }
                [RegularExpression(@"^\d{3}-?\d{3}-?\d{4}$")]
                public object Phone { get; set; }
                [Required()]
                [DataType(DataType.EmailAddress)]
                [RegularExpression(@"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$")]
                public object Email { get; set; }
            }
        }
    } 


    在 MVC 中的最佳做法是不在控制器中包括实体数据模型 (EDM) 或其他任何数据访问框架代码。 而应使用存储库模式。 存储库位于应用程序与数据存储之间。 存储库将业务逻辑与和基础数据的交互分离,并关注一个区域中的数据访问,使其更易于创建和维护。

    存储库从域模型返回对象。 对于简单模型(如在此演练中使用的模型),从 EDM、LINQ to SQL 和其他数据模型返回的对象可以成为合格的域对象。

    对于更复杂的应用程序,可能需要一个映射层。 映射层不一定低效。 LINQ 提供程序可创建对后端数据存储的高效查询(即,它们可以使用最少数量的中间对象进行查询)。

    使用存储库不应需要有关 EDM、LINQ to SQL 或您使用的其他任何数据模型的知识。 (尽管本演练未涉及 LINQ,但使用 LINQ 作为查询抽象意味着可以隐藏数据存储机制。 例如,这样您可将 SQL Server 用于生产,将 LINQ to Objects 用于内存中集合,以进行测试。)

    测试直接访问 EDM 的控制器中的操作方法需要连接数据库,原因是这些操作方法依赖于 EDM(它依赖于数据库)。 下面的代码显示一个 MVC 控制器,它直接使用 EDM 的 Contact 实体,并提供简单示例以说明为何操作方法中的混合数据库调用会使操作方法难于测试。 例如,用于编辑和删除数据的单元测试会更改数据库的状态。 这需要每次通过单元测试都具有全新的数据库设置。 此外,数据库调用成本极高,而单元测试应为轻量型的,因此在开发应用程序时可频繁运行。

    public class NotTDDController : Controller {
    
        ContactEntities _db = new ContactEntities();
    
        public ActionResult Index() {
            var dn = _db.Contacts;
            return View(dn);
        }
    
        public ActionResult Edit(int id) {
            Contact prd = _db.Contacts.FirstOrDefault(d => d.Id == id);
            return View(prd);
        }
    
        [HttpPost]
        public ActionResult Edit(int id, FormCollection collection) {
            Contact prd = _db.Contacts.FirstOrDefault(d => d.Id == id);
            UpdateModel(prd);
            _db.SaveChanges();
            return RedirectToAction("Index");
        }
    } 

    存储库模式有以下优点:

    • 它为单元测试提供了替代点。 可以轻松测试业务逻辑,无需数据库和其他外部依赖项。

    • 可将重复的查询和数据访问模式移除并重构到存储库中。

    • 控制器方法可使用强类型参数,这意味着每次编译时编译器均可发现数据类型错误,而不依赖于在测试过程中的运行时发现数据类型错误。

    • 集中进行数据访问,这具有以下优点:

      • 任务分离 (SoC) 更大,这是 MVC 的另一个原则,可提高可维护性和可靠性。

      • 集中数据缓存的简化实现。

      • 更为灵活、耦合性更小的体系结构,可用作应用程序的总体设计。

    • 行为可与相关数据关联。 例如,可以计算字段或在实体的数据元素之间强制实施复杂关系或业务规则。

    • 可以应用域模型来简化复杂的业务逻辑。

    将存储库模式用于 MVC 和 TDD 通常需要为数据访问类创建接口。 在对控制器方法进行单元测试时,存储库接口将便于注入 mock 存储库。

    在本节中,您将添加一个联系人存储库,它是用于在数据库中保存联系人的类。 您还要为联系人存储库添加一个接口。

    添加存储库

    1. 在 MvcContacts\Models 文件夹中,创建一个类文件并添加一个名为 IContactRepository 的类。

      IContactRepository 类将包含用于存储库对象的接口。

    2. 使用以下代码替换类文件中的代码:

  9. using System;
    using System.Collections.Generic;
    
    namespace MvcContacts.Models {
        public interface IContactRepository {
            void CreateNewContact(Contact contactToCreate);
            void DeleteContact(int id);
            Contact GetContactByID(int id);
            IEnumerable<Contact> GetAllContacts();
            int SaveChanges();
    
        }
    } 
    1. 在 MvcContacts\Models 文件夹中,创建一个名为 EntityContactManagerRepository 的新类。

      EntityContactManagerRepository 类将实现用于存储库对象的 IContactRepository 接口。

    2. 将 EntityContactManagerRepository 类中的代码替换为以下代码:

    using System.Collections.Generic;
    using System.Linq;
    
    namespace MvcContacts.Models {
        public class EF_ContactRepository : MvcContacts.Models.IContactRepository {
    
            private ContactEntities _db = new ContactEntities();
    
            public Contact GetContactByID(int id) {
                return _db.Contacts.FirstOrDefault(d => d.Id == id);
            }
    
            public IEnumerable<Contact> GetAllContacts() {
                return _db.Contacts.ToList();
            }
    
            public void CreateNewContact(Contact contactToCreate) {
                _db.AddToContacts(contactToCreate);
                _db.SaveChanges();
             //   return contactToCreate;
            }
    
            public int SaveChanges() {
                return _db.SaveChanges();
            }
    
            public void DeleteContact(int id) {
                var conToDel = GetContactByID(id);
                _db.Contacts.DeleteObject(conToDel);
                _db.SaveChanges();
            }
    
        }
    } 

    在本节中,您将添加存储库的模拟实现,添加单元测试,并从单元测试中实现应用程序功能。

    实现内存中存储库

    1. 在 MvcContacts.Tests 项目中,创建一个 Models 文件夹。

    2. 在 MvcContacts.Tests\Models 文件夹中,创建一个名为 InMemoryContactRepository 的新类。

      InMemoryContactRepository 类将实现您以前创建的 IContactRepository 接口并具有一个用于驱动应用程序设计的简单存储库。

    3. 将 InMemoryContactRepository 类中的代码替换为以下代码:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using MvcContacts.Models;
    
    namespace MvcContacts.Tests.Models {
        class InMemoryContactRepository : MvcContacts.Models.IContactRepository {
            private List<Contact> _db = new List<Contact>();
    
            public Exception ExceptionToThrow { get; set; }
            //public List<Contact> Items { get; set; }
    
            public void SaveChanges(Contact contactToUpdate) {
    
                foreach (Contact contact in _db) {
                    if (contact.Id == contactToUpdate.Id) {
                        _db.Remove(contact);
                        _db.Add(contactToUpdate);
                        break;
                    }
                }
            }
    
            public void Add(Contact contactToAdd) {
                _db.Add(contactToAdd);
            }
    
            public Contact GetContactByID(int id) {
                return _db.FirstOrDefault(d => d.Id == id);
            }
    
            public void CreateNewContact(Contact contactToCreate) {
                if (ExceptionToThrow != null)
                    throw ExceptionToThrow;
    
                _db.Add(contactToCreate);
               // return contactToCreate;
            }
    
            public int SaveChanges() {
                return 1;
            }
    
            public IEnumerable<Contact> GetAllContacts() {
                return _db.ToList();
            }
    
    
            public void DeleteContact(int id) {
                _db.Remove(GetContactByID(id));
            }
    
        }
    } 
    

    添加测试支持

    1. 在 MvcContacts 项目中,打开 Controllers\HomeController.cs 文件。 用下面的代码替换 Controllers\HomeController.cs 文件中的代码:

    using System;
    using System.Web.Mvc;
    using MvcContacts.Models;
    
    namespace MvcContacts.Controllers {
        [HandleError]
        public class HomeController : Controller {
            IContactRepository _repository;
            public HomeController() : this(new EF_ContactRepository()) { }
            public HomeController(IContactRepository repository) {
                _repository = repository;
            }
            public ViewResult Index() {
                throw new NotImplementedException();
            }
        }
    }
    1. 此类包含两个构造函数。 一个是无参数构造函数。 另一个采用 IContactRepository 类型的参数;单元测试将使用此构造函数传入 mock 存储库。 无参数构造函数将创建 EF_ContactRepository 类的一个实例,并在调用控制器中的操作方法时被 MVC 管道调用。

    2. 关闭 HomeController 文件。

    3. 在 MvcContacts.Test 项目中,打开 Controllers\HomeControllerTest 文件并用下面的代码替换该文件中的代码:

      using System.Web.Mvc;
      using Microsoft.VisualStudio.TestTools.UnitTesting;
      using MvcContacts.Controllers;
      
      using MvcContacts.Models;
      using MvcContacts.Tests.Models;
      using System.Web;
      using System.Web.Routing;
      using System.Security.Principal;
      
      namespace MvcContacts.Tests.Controllers {
          [TestClass]
          public class HomeControllerTest {
              
              Contact GetContact() {
                  return GetContact(1, "Janet", "Gates");
              }
      
              Contact GetContact(int id, string fName, string lName) {
                  return new Contact
                  {
                      Id = id,
                      FirstName = fName,
                      LastName = lName,
                      Phone = "710-555-0173",
                      Email = "janet1@adventure-works.com"
                  };
              }
      
              private static HomeController GetHomeController(IContactRepository repository) {
                  HomeController controller = new HomeController(repository);
      
                  controller.ControllerContext = new ControllerContext()
                  {
                      Controller = controller,
                      RequestContext = new RequestContext(new MockHttpContext(), new RouteData())
                  };
                  return controller;
              }
      
      
              private class MockHttpContext : HttpContextBase {
                  private readonly IPrincipal _user = new GenericPrincipal(
                           new GenericIdentity("someUser"), null /* roles */);
      
                  public override IPrincipal User {
                      get {
                          return _user;
                      }
                      set {
                          base.User = value;
                      }
                  }
              }
          }
      }
      1. 前一示例中的代码包含两个用于获取联系人 (GetContact) 的方法重载和一个用于获取 HomeController 对象的方法。 单元测试将调用 HomeController 中的方法。 该代码还包含一个用于模拟 HttpContext 对象的类。 在此示例中使用的 MockHttpContext 类是在您使用单元测试创建新 MVC 项目时创建的 AcountControllerTest 类文件中找到的类的简化版本。

      TDD 和 MVC 的原则之一是每个测试应在操作方法中驱动特定需求。 测试不应验证数据库或其他组件(但应在数据访问单元测试和集成测试中测试这些组件)。 另一个目标是测试名称应具有充分的说明性;当您有成百上千个测试时,通过诸如 Creat_Post_Test1 之类的短常规名称将难于了解测试。

      在演练的此部分中,将假定主控制器的默认方法的设计调用会返回联系人的列表。 控制器的默认方法为 Index,所以第一个测试将验证控制器是否返回索引视图。 当您在本演练早期对 Index 方法进行更改时,已将其更改为返回 ViewResult 对象,而不是更常规的 ActionResult 对象。 当您知道一种方法始终会返回 ViewResult 对象时,可以通过从控制器方法中返回 ViewResult 对象来简化单元测试。 当您返回 ViewResult 对象时,单元测试不必在测试过程中将典型的 ActionResult 对象转换为 ViewResult 对象,从而使测试更简单、可读性更好。

      添加第一个测试

      1. 在 HomeControllerTest 类中,添加一个名为 Index_Get_AsksForIndexView 的单元测试,该测试可验证 Index 方法是否返回名为 Index 的视图。

        下面的示例演示完成的单元测试。

    [TestMethod]
    public void Index_Get_AsksForIndexView() {
        // Arrange
        var controller = GetHomeController(new InMemoryContactRepository());
        // Act
        ViewResult result = controller.Index();
        // Assert
        Assert.AreEqual("Index", result.ViewName);
    } 
    
    
    1. 在“测试”菜单上,单击“运行”,然后单击“解决方案中的所有测试”。

      结果将显示在“测试结果”窗口中。 正如预期那样,Index_Get_AsksForIndexView 单元测试失败。

    2. 实现 HomeController 类的以下 Index 方法,以便返回所有联系人的列表。

    public ViewResult Index() {
                return View("Index", _repository.ListContacts());
            }

    在本节中,您将验证是否可以检索所有联系人。 您不想创建测试数据访问的单元测试。 验证应用程序是否可以访问数据库并检索联系人是非常重要的,但这是集成测试,而不是 TDD 单元测试。

    为检索联系人添加测试

    • 创建一个测试,它可在 HomeControllerTest 类中向内存中存储库添加两个联系人,然后验证 Index 视图中包含的 ViewDataModel 对象中是否包含这些联系人。

      下面的示例演示完成的测试。

    [TestMethod]
    public void Index_Get_RetrievesAllContactsFromRepository() {
        // Arrange
        Contact contact1 = GetContactNamed(1, "Orlando", "Gee");
        Contact contact2 = GetContactNamed(2, "Keith", "Harris");
        InMemoryContactRepository repository = new InMemoryContactRepository();
        repository.Add(contact1);
        repository.Add(contact2);
        var controller = GetHomeController(repository);
    
        // Act
        var result = controller.Index();
    
        // Assert
        var model = (IEnumerable<Contact>)result.ViewData.Model;
        CollectionAssert.Contains(model.ToList(), contact1);
        CollectionAssert.Contains(model.ToList(), contact1);
    } 

    现在,您将测试创建新联系人的过程。 第一个测试验证 HTTP POST 操作是否已成功完成并使用特意包含模型错误的数据调用 Create 方法。 结果将不添加新联系人,而是返回包含您已输入的字段和模型错误的 HTTP GET Create 视图。 对控制器运行单元测试不会执行 MVC 管道或模型绑定过程。 因此,绑定过程中将不捕获模型错误。 为了考虑此情况,测试添加了模拟错误。

    为创建联系人添加测试

    1. 将下列测试添加到该项目中:

    [TestMethod]
    public void Create_Post_ReturnsViewIfModelStateIsNotValid() {
        // Arrange
        HomeController controller = GetHomeController(new InMemoryContactRepository());
        // Simply executing a method during a unit test does just that - executes a method, and no more. 
        // The MVC pipeline doesn't run, so binding and validation don't run.
        controller.ModelState.AddModelError("", "mock error message");
        Contact model = GetContactNamed(1, "", "");
    
        // Act
        var result = (ViewResult)controller.Create(model);
    
        // Assert
        Assert.AreEqual("Create", result.ViewName);
    } 
    1. 该代码演示尝试添加包含模型错误的联系人时,如何返回 HTTP GET Create 视图。

    2. 添加下列测试:

    [TestMethod]
    public void Create_Post_PutsValidContactIntoRepository() {
        // Arrange
        InMemoryContactRepository repository = new InMemoryContactRepository();
        HomeController controller = GetHomeController(repository);
        Contact contact = GetContactID_1();
    
        // Act
        controller.Create(contact);
    
        // Assert
        IEnumerable<Contact> contacts = repository.GetAllContacts();
        Assert.IsTrue(contacts.Contains(contact));
    } 
    
    1. 该代码演示如何验证 Create 方法的 HTTP POST 是否向存储库中添加有效联系人。

    一个经常被忽略的测试是验证方法是否正确处理异常。 通过 InMemoryContactRepository 类,可以设置 mock 异常,模拟当约束或其他违规发生时数据库将引发的异常。 模型验证无法捕获许多数据库异常,因此验证异常处理代码是否正常工作是很重要的。 下面的示例演示如何执行此操作。

    [TestMethod]
    public void Create_Post_ReturnsViewIfRepositoryThrowsException() {
        // Arrange
        InMemoryContactRepository repository = new InMemoryContactRepository();
        Exception exception = new Exception();
        repository.ExceptionToThrow = exception;
        HomeController controller = GetHomeController(repository);
        Contact model = GetContactID_1();
    
        // Act
        var result = (ViewResult)controller.Create(model);
    
        // Assert
        Assert.AreEqual("Create", result.ViewName);
        ModelState modelState = result.ViewData.ModelState[""];
        Assert.IsNotNull(modelState);
        Assert.IsTrue(modelState.Errors.Any());
        Assert.AreEqual(exception, modelState.Errors[0].Exception);
    } 

    可下载的示例包括更多此处未详细介绍的测试。 若要了解有关如何使用 mock 对象以及如何将 TDD 方法用于 MVC 项目的更多信息,请查看其他测试,并为 Delete 和 Edit 方法编写测试。

    本文转载自:MSDN - 演练:将 TDD 和 ASP.NET MVC 结合使用

分享到:

发表评论

评论列表