这是一个我非常感兴趣的话题。有很多纯粹主义者认为不应该测试EF和NHibernate等技术,他们是正确的,因为它们已经经过了严格的测试,正如之前的答案所述,花费大量时间测试你不拥有的东西通常是没有意义的。
然而,你拥有数据库底层! 这就是在我看来这种方法存在问题的地方,你不需要测试EF/NH是否正常工作,而是需要测试你的映射/实现是否与你的数据库配合工作。在我看来,这是系统中最重要的部分之一。
但严格来说,我们正在从单元测试领域进入集成测试领域,但原则仍然相同。
您需要做的第一件事是能够模拟您的数据访问层(DAL),以便于EF和SQL独立测试。 这些是您的单元测试。接下来,您需要设计您的集成测试来证明您的数据访问层,我认为这些同样重要。
有几点需要考虑:
- 您的数据库需要在每个测试中处于已知状态。大多数系统使用备份或创建脚本进行此操作。
- 每个测试必须是可重复的
- 每个测试必须是原子性的
设置数据库的两种主要方法,第一种是运行一个单元测试创建DB脚本。这确保了您的单元测试数据库始终处于每个测试的相同状态(您可以将其重置或在事务中运行每个测试以确保这一点)。
您的另一个选项是我使用的:针对每个单独的测试运行特定设置。我认为这是最好的方法,原因有两个:
- 您的数据库更简单,您不需要为每个测试创建整个模式
- 每个测试更安全,如果您更改创建脚本中的一个值,它不会使数十个其他测试失效。
不幸的是,您在此处的妥协是速度。运行所有这些测试、运行所有这些设置/拆卸脚本需要时间。
最后一个要点,编写如此大量的SQL来测试您的ORM可能非常艰难。这就是我采用一种非常不好的方法(纯粹主义者可能不同意我)。我使用我的ORM来创建测试!与其在系统中为每个DAL测试编写单独的脚本,我有一个测试设置阶段,其中创建对象,将它们附加到上下文并保存它们。然后我运行我的测试。
这远非理想的解决方案,但在实践中,我发现它更容易管理(特别是当您有几千个测试时),否则您将创建大量的脚本。实用性胜过纯洁度。
毫无疑问,我几年后(甚至几个月/天)会回顾这个答案并不同意自己的方法,因为我的方法已经改变了 - 但这是我的当前方法。
尝试总结我上面所说的一切,这是我的典型DB集成测试:
[Test]
public void LoadUser()
{
this.RunTest(session =>
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
return user.UserID;
}, id =>
{
var user = LoadMyUser(id);
Assert.AreEqual("Mr", user.Title);
Assert.AreEqual("Joe", user.Firstname);
Assert.AreEqual("Bloggs", user.Lastname);
}
}
需要注意的关键点是这两个循环的会话是完全独立的。在你实现RunTest时,必须确保上下文已提交和销毁,并且第二部分的数据只能来自你的数据库。
编辑 2014年10月13日
我说过我可能会在接下来的几个月里修改这个模型。虽然我大体上支持上述方法,但我稍微更新了我的测试机制。我现在倾向于在TestSetup和TestTearDown中创建实体。
[SetUp]
public void Setup()
{
this.SetupTest(session =>
{
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);
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);
}
这种方法有几个原因:
- 没有额外的数据库调用(一个安装,一个拆卸)
- 测试更加细粒度,每个测试验证一个属性
- 设置/清除逻辑从测试方法中移除
我认为这使得测试类更简单,测试更加细粒度(单个断言是好的)
编辑 2015年5月3日
这种方法的另一种修订。虽然类级别的设置非常有助于加载属性等测试,但它们在需要不同设置的情况下不太有用。在这种情况下,为每种情况设置新类是过度的。
为了解决这个问题,我现在倾向于有两个基本类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);
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());
});
}
}
总的来说,这两种方法都有效,具体取决于您要测试什么。