ASP.NET MVC Tip #20 – 如何对 Data Access 进行单元测试
ASP.NET MVC Tip #20 – 如何对 Data Access 进行单元测试
ASP.NET MVC Tip #20 – How to Unit Test Data Access
美语原文:http://weblogs.asp.net/stephenwalther/archive/2008/07/15/asp-net-mvc-tip-20-how-to-unit-test-data-access.aspx
国语翻译:http://www.cnblogs.com/mike108mvp
译者注:在下水平有限,翻译中若有错误或不妥之处,欢迎大家批评指正。谢谢。
译者注:ASP.NET MVC QQ交流群 1215279 欢迎对 ASP.NET MVC 感兴趣的朋友加入
在这篇帖子中,我将演示如何为MVC controller action中的数据访问编写单元测试。我将展示如何创建对LINQ to SQL controller action 代码进行单元测试。
我编写的大部分ASP.NET MVC applications 都包含大量的数据访问代码。特别是,我使用微软的LINQ to SQL来执行数据操作。如何单元测试这些数据访问代码?
对于这个问题这里有一些不同的方法:
(1) 不对数据访问代码进行单元测试。
(2) 当单元测试数据访问代码时,创建一个测试数据库。
(3) 当单元测试数据访问代码时,伪造(Fake)DataContext。
很多“测试驱动开发(TDD)”社区的成员都主张,你不应该对数据访问代码进行单元测试。例如,Michael Feathers 在他写的优秀图书《Working Effectively with Legacy Code》中主张,当你在执行“测试驱动开发(TDD)”时,你不应该对数据访问代码进行单元测试。根据他的观点,一个单元测试必须在十分之一秒内执行。因为数据访问代码执行起来太慢了,所以你就不应该对它进行单元测试。
第二种观点是每次你运行一个单元测试时,创建一个测试数据库。这也是我在这篇帖子中将会采用的方法。在这篇帖子中,我将展示如何从一个DataContext 中自动生成一个测试数据库。
最后一种方式,你可以在内存数据库中伪造一个DataContext。事实上,我认为这才是最好的方法。这种方法会让Michael Feathers很happy,因为它能够让你编写执行速度很快的单元测试。我将在未来的帖子中探讨这个方法。
一个简单的数据访问MVC Web Application
当进行测试驱动开发时,你应该先编写你的测试代码,然后再针对测试代码进行开发编码。这种开发方式强迫你从使用你代码的人的视角来编写你的代码。
因为我在这篇帖子中主要是给你演示如何在ASP.NET MVC application中对数据访问代码进行单元测试,所以我将违反良好的TDD实践原则,先写我的开发代码(而不是先写测试代码)。请原谅我的违规。
代码清单1中的HomeController暴露了两个action。第一个action叫做Index(),返回movie的一批数据库记录。第二个action叫做InsertMovie(), 添加一个新的movie到数据库中。Index() 和 InsertMovie()方法都使用LINQ to SQL来访问数据库。
注意HomeController类有两个构造函数。第一个构造函数接收一个LINQ to SQL DataContext参数。第二个构造函数是没有参数的,它创建了一个DataContext 并且将它传递给第一个构造函数。
第二个无参构造函数将在MVC application实际运行时被HomeController 调用。单元测试将利用这个构造函数来取得DataContext 参数。那样的话,一个单元测试将传递一个测试用的DataContext 而不是一个实际的DataContext 。
Listing 1 – HomeController.cs (C#)
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Tip20.Models;
namespace Tip20.Controllers
{
public class HomeController : Controller
{
private MovieDataContext _dataContext;
public HomeController(MovieDataContext dataContext)
{
_dataContext = dataContext;
}
public HomeController() : this(new MovieDataContext())
{ }
public ActionResult Index()
{
var movies = _dataContext.Movies.OrderByDescending(m => m.Id);
return View(movies);
}
public ActionResult InsertMovie(string title, string director)
{
var newMovie = new Movie();
newMovie.Title = title;
newMovie.Director = director;
newMovie.DateReleased = DateTime.Parse("12/25/1966");
_dataContext.Movies.InsertOnSubmit(newMovie);
_dataContext.SubmitChanges();
return RedirectToAction("Index");
}
}
}
创建一个DataContext 单元测试基类
那么如何为HomeController 类创建单元测试呢?在这个部分,我将解释如何创建一个DataContextUnitTest 基类,你可以使用它做为基类来对使用LINQ to SQL的controller actions进行单元测试。
DataContextUnitTest类包含在代码清单2中。
Listing 2 – DataContextUnitTest.cs (C#)
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Data.SqlClient;
using System.Reflection;
using System.IO;
public abstract class DataContextUnitTest<T> where T: DataContext
{
const string TestDBPath = @"C:\Users\swalther\Documents\Common Content\Blog\Tip20 Linq to SQL CreateDatabase\CS\Tip20Tests\App_Data\Test.mdf";
protected T TestDataContext { get; set; }
[TestInitialize]
public void Initialize()
{
this.CreateTestDB();
}
public void CreateTestDB()
{
var testConnectionString = GetTestConnectionString();
// Need to use reflection here since you
// cannot use Generics with a contructors that require params
Type[] types = {typeof(string)};
Object[] typeValues = { testConnectionString };
this.TestDataContext = (T)typeof(T).GetConstructor(types).Invoke(typeValues);
this.RemoveTestDB();
this.TestDataContext.CreateDatabase();
}
[TestCleanup]
public void Cleanup()
{
this.RemoveTestDB();
}
protected void RemoveTestDB()
{
if (this.TestDataContext.DatabaseExists())
this.TestDataContext.DeleteDatabase();
}
private static string GetTestConnectionString()
{
var conBuilder = new SqlConnectionStringBuilder();
conBuilder.AttachDBFilename = TestDBPath;
conBuilder.DataSource = @".\SQLExpress";
conBuilder.IntegratedSecurity = true;
conBuilder.UserInstance = true;
return conBuilder.ConnectionString;
}
}
在使用DataContextUnitTest 类之前,你需要添加System.Data.Linq 和 System.Data 程序集的引用到你的测试项目中。
注意DataContextUnitTest 是一个泛型类。当创建一个该类的实例时,你必须指定该类代表的DataContext的类型。变量T代表了DataContext的类型。
此外还要注意,DataContextUnitTest 类包含了一个常量TestDBPath。你必须设置这个常量的值为你创建的测试数据库的路径。记住你下载这篇帖子的代码并想在你自己的项目中应用的话,你要修改这个常量。
DataContextUnitTest类包含一个Initialize()方法,它是用TestInitialize特性来装饰的。该特性使这个方法在每次单元测试前被执行。这个Initialize()方法创建了一个新的测试用的DataContext 并生成了一个新的数据库。该数据库是由DataContext 类中的CreateDatabase() 方法创建的。
DataContextUnitTest 类也包含一个Cleanup()方法,用TestCleanup特性来装饰。在每个单元测试被执行之后,该测试数据库将被销毁。DataContext.DeleteDatabase()方法是用来在硬盘上销毁测试数据库文件的。
你可以使用DataContextUnitTest 类作为任何对controller 数据访问进行单元测试的基类。例如,代码清单3中的类包含了两个针对HomeController 的单元测试。这两个单元测试,IndexMovieCount()和IndexInsertMovie(),是用TestMethod 特性来装饰的。
Listing 3 – HomeControllerTest.cs (C#)
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tip20.Controllers;
using Tip20.Models;
using System.Data.Linq;
using System.Linq;
namespace Tip20Tests.Controllers
{
[TestClass]
public class HomeControllerTest : DataContextUnitTest<MovieDataContext>
{
public Movie CreateTestMovie(string title, string director)
{
var newMovie = new Movie();
newMovie.Title = title;
newMovie.Director = director;
newMovie.DateReleased = DateTime.Parse("12/25/1966");
return newMovie;
}
public void AddTestData()
{
var newMovie1 = this.CreateTestMovie("Star Wars", "George Lucas");
this.TestDataContext.Movies.InsertOnSubmit(newMovie1);
var newMovie2 = this.CreateTestMovie("Ghost Busters", "Ivan Reitman");
this.TestDataContext.Movies.InsertOnSubmit(newMovie2);
this.TestDataContext.SubmitChanges();
}
[TestMethod]
public void IndexMovieCount()
{
// Arrange
this.AddTestData();
HomeController controller = new HomeController(this.TestDataContext);
// Act
ViewResult result = controller.Index() as ViewResult;
// Assert
var model = (IQueryable<Movie>)result.ViewData.Model;
Assert.AreEqual(2, model.Count());
}
[TestMethod]
public void IndexInsertMovie()
{
// Arrange
HomeController controller = new HomeController(this.TestDataContext);
// Act
var title = "King Kong";
var director = "Peter Jackson";
controller.InsertMovie(title, director);
// Assert
var results = from m in this.TestDataContext.Movies
where m.Title == title && m.Director == director select m;
Assert.AreEqual(1, results.Count());
}
}
}
注意HomeControllerTest 类是从DataContextUnitTest 基类派生来的。MovieDataContext的类型被传递给泛型基类。
第一个IndexMovieCount()单元测试方法是用来测试Index() controller action 是否正确地从数据库中返回了一批movie 数据记录。首先,这个测试方法插入了两条movies 记录到数据库中,然后,HomeController.Index()方法被调用,来测试Index()方法返回的数据记录数目是否正确。如果返回了两条记录,则测试成功。
第二个IndexInsertMovie()单元测试方法是用来测试一条新的数据记录是否被正确地插入数据库中。这个方法调用HomeController.InsertMovie()方法来插入一条新的movie记录,然后,该测试方法尝试从测试数据库中取得刚才插入的那条记录。
总结
在这篇帖子中,我演示了一种对MVC controller actions的数据访问进行单元测试的方法。我演示了如何从LINQ to SQL DataContext中自动生成一个测试数据库。同时我也展示了如何创建一个对执行LINQ to SQL查询的controller actions进行单元测试的标准基类。
In this tip, I demonstrated one approach for unit testing MVC controller actions that access a database. I demonstrated how you can generate a test database from a LINQ to SQL DataContext automatically. I showed you how you can create a standard base class for unit testing controller actions that perform LINQ to SQL queries.
下载代码:http://weblogs.asp.net/blogs/stephenwalther/Tip20/Tip20.zip
posted on 2008-07-20 19:54 mike108mvp 阅读(1497) 评论(0) 编辑 收藏 举报