这是我非常感兴趣的主题。许多纯粹主义者说,您不应该测试EF和NHibernate等技术。它们是正确的,它们已经过非常严格的测试,并且如先前的回答所述,花费大量时间测试您不拥有的东西通常毫无意义。
但是,您确实拥有数据库!在我看来,这就是这种方法失效的地方,您无需测试EF / NH是否能正确执行其工作。您需要测试您的映射/实现是否与数据库一起使用。在我看来,这是您可以测试的系统中最重要的部分之一。
但是严格来说,我们正在脱离单元测试的领域,而进入集成测试,但是原理保持不变。
您需要做的第一件事是能够模拟DAL,以便可以独立于EF和SQL来测试BLL。这些是您的单元测试。接下来,您需要设计集成测试以证明您的DAL,在我看来,这些都很重要。
有几件事情要考虑:
- 您的数据库在每次测试时都必须处于已知状态。大多数系统为此使用备份或创建脚本。
- 每个测试必须是可重复的
- 每个测试必须是原子的
设置数据库有两种主要方法,第一种是运行UnitTest创建数据库脚本。这样可确保您的单元测试数据库在每次测试开始时始终处于相同状态(您可以重置此状态或在事务中运行每个测试以确保这一点)。
您的另一选择是我要做的,为每个测试运行特定的设置。我认为这是最好的方法,主要有两个原因:
- 您的数据库更简单,您不需要为每个测试使用完整的架构
- 每个测试都比较安全,如果您在创建脚本中更改一个值,它不会使其他几十个测试无效。
不幸的是,您的妥协是速度。运行所有这些测试,运行所有这些设置/拆卸脚本需要花费时间。
最后一点,编写如此大量的SQL以测试您的ORM可能非常艰巨。这是我采取的一种非常讨厌的方法(这里的纯粹主义者会不同意我的观点)。我使用ORM来创建测试!我没有在系统中为每个DAL测试使用单独的脚本,而是在测试设置阶段创建对象,将对象附加到上下文并保存它们。然后,我运行测试。
这远不是理想的解决方案,但是在实践中,我发现这很容易管理(尤其是当您有数千个测试时),否则您将创建大量的脚本。实用胜过纯度。
毫无疑问,我将在几年(数月/日)后回头看一下这个答案,并且随着方法的改变,我也不同意我的看法,但这是我目前的方法。
为了总结以上我所说的一切,这是我典型的数据库集成测试:
[Test]
public void LoadUser()
{
this.RunTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
return user.UserID;
}, id => // the ID of the entity we need to load
{
var user = LoadMyUser(id); // load the entity
Assert.AreEqual("Mr", user.Title); // test your properties
Assert.AreEqual("Joe", user.Firstname);
Assert.AreEqual("Bloggs", user.Lastname);
}
}
这里要注意的关键是两个循环的会话是完全独立的。在RunTest的实现中,必须确保上下文已提交并销毁,并且对于第二部分,您的数据只能来自数据库。
编辑13/10/2014
我确实说过,我可能会在接下来的几个月中修改此模型。虽然我在很大程度上支持我上面提倡的方法,但是我已经稍微更新了我的测试机制。我现在倾向于在TestSetup和TestTearDown中创建实体。
[SetUp]
public void Setup()
{
this.SetupTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
});
}
[TearDown]
public void TearDown()
{
this.TearDownDatabase();
}
然后分别测试每个属性
[Test]
public void TestTitle()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Mr", user.Title);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Bloggs", user.Lastname);
}
这种方法有几个原因:
- 没有其他数据库调用(一种设置,一种拆卸)
- 测试更加精细,每个测试都验证一个属性
- Setup / TearDown逻辑已从Test方法本身中删除
我觉得这使测试类更简单,测试更精细(单个断言是好的)
编辑5/3/2015
关于此方法的另一种修订。虽然类级别的设置对于诸如加载属性之类的测试非常有帮助,但是在需要不同设置的情况下它们的用处就不大。在这种情况下,为每种情况设置一个新的类是过分的。
为了解决这个问题,我现在倾向于有两个基类SetupPerTest
和SingleSetup
。这两个类根据需要公开了该框架。
在SingleSetup
第一次编辑中,我们有一个非常类似的机制。一个例子是
public TestProperties : SingleSetup
{
public int UserID {get;set;}
public override DoSetup(ISession session)
{
var user = new User("Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Bloggs", user.Lastname);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
}
但是,确保只加载正确的实体的引用可以使用SetupPerTest方法
public TestProperties : SetupPerTest
{
[Test]
public void EnsureCorrectReferenceIsLoaded()
{
int friendID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriend();
session.Save(user);
friendID = user.Friends.Single().FriendID;
} () =>
{
var user = GetUser();
Assert.AreEqual(friendID, user.Friends.Single().FriendID);
});
}
[Test]
public void EnsureOnlyCorrectFriendsAreLoaded()
{
int userID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriends(2);
var user2 = CreateUserWithFriends(5);
session.Save(user);
session.Save(user2);
userID = user.UserID;
} () =>
{
var user = GetUser(userID);
Assert.AreEqual(2, user.Friends.Count());
});
}
}
总之,这两种方法都可以工作,具体取决于您要测试的内容。