Answers:
我建议模拟一下您对数据库的调用。从本质上讲,它们是对象,看起来像您尝试在其上调用方法的对象,它们具有与调用者相同的属性,方法等。但是,当调用特定方法时,它没有执行程序编程要执行的任何操作,而是完全跳过了该操作,仅返回结果。该结果通常由您提前定义。
为了设置对象以进行模拟,您可能需要使用某种控制/依赖项注入模式的反转,如以下伪代码所示:
class Bar
{
private FooDataProvider _dataProvider;
public instantiate(FooDataProvider dataProvider) {
_dataProvider = dataProvider;
}
public getAllFoos() {
// instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
return _dataProvider.GetAllFoos();
}
}
class FooDataProvider
{
public Foo[] GetAllFoos() {
return Foo.GetAll();
}
}
现在,在单元测试中,您将创建一个FooDataProvider的模拟,该模拟使您可以调用GetAllFoos方法,而不必实际访问数据库。
class BarTests
{
public TestGetAllFoos() {
// here we set up our mock FooDataProvider
mockRepository = MockingFramework.new()
mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);
// create a new array of Foo objects
testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}
// the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
// instead of calling to the database and returning whatever is in there
// ExpectCallTo and Returns are methods provided by our imaginary mocking framework
ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)
// now begins our actual unit test
testBar = new Bar(mockFooDataProvider)
baz = testBar.GetAllFoos()
// baz should now equal the testFooArray object we created earlier
Assert.AreEqual(3, baz.length)
}
}
简而言之,是一种常见的模拟场景。当然,您仍然可能还要对实际的数据库调用进行单元测试,为此您需要访问数据库。
理想情况下,您的对象应该是持久性的。例如,您应该有一个“数据访问层”,您可以向该数据访问层发出请求,并返回对象。这样,您可以将该部分放在单元测试之外,也可以单独进行测试。
如果您的对象与数据层紧密耦合,则很难进行适当的单元测试。单元测试的第一部分是“单元”。所有单元都应该能够进行隔离测试。
在我的c#项目中,我将NHibernate与完全独立的Data层一起使用。我的对象位于核心域模型中,可以从我的应用程序层进行访问。应用程序层与数据层和域模型层都进行对话。
应用程序层有时也称为“业务层”。
如果使用的是PHP,请创建一组特定的类,仅用于数据访问。确保您的对象不知道它们如何持久保存,并在您的应用程序类中连接二者。
另一种选择是使用模拟/存根。
对具有数据库访问权限的对象进行单元测试的最简单方法是使用事务作用域。
例如:
[Test]
[ExpectedException(typeof(NotFoundException))]
public void DeleteAttendee() {
using(TransactionScope scope = new TransactionScope()) {
Attendee anAttendee = Attendee.Get(3);
anAttendee.Delete();
anAttendee.Save();
//Try reloading. Instance should have been deleted.
Attendee deletedAttendee = Attendee.Get(3);
}
}
这将还原数据库的状态,基本上就像事务回滚一样,因此您可以根据需要多次运行测试,而不会产生任何副作用。我们已经在大型项目中成功使用了这种方法。我们的构建确实花费了一些时间(15分钟),但是对于进行1800个单元测试并不可怕。此外,如果需要考虑构建时间,则可以将构建过程更改为具有多个构建,一个用于构建src,另一个用于随后处理单元测试,代码分析,打包等操作。
当我们开始着眼于对包含大量“业务逻辑” sql操作的中间层过程进行单元测试时,我也许可以带您体验一下我们的经验。
我们首先创建了一个抽象层,该抽象层允许我们“插入”任何合理的数据库连接(在我们的情况下,我们仅支持单个ODBC类型的连接)。
一旦到位,我们便可以在代码中执行类似的操作(我们使用C ++,但是我确定您能理解这个想法):
GetDatabase()。ExecuteSQL(“ INSERT INTO foo(blah,blah)”)
在正常运行时,GetDatabase()将返回一个对象,该对象通过ODBC直接将所有SQL(包括查询)喂入数据库。
然后,我们开始研究内存数据库-从长远来看,最好的数据库似乎是SQLite。(http://www.sqlite.org/index.html)。它的设置和使用非常简单,它允许我们子类并重写GetDatabase()来将sql转发到为每个执行的测试创建和销毁的内存数据库。
我们仍处于初期阶段,但到目前为止看起来还不错,但是我们必须确保创建所需的任何表并用测试数据填充它们-但是我们通过创建以下方法在这里减轻了工作量一组通用的辅助功能,可以为我们做很多这一切。
总体而言,它对我们的TDD流程有极大帮助,因为由于sql /数据库的本质,进行似乎无害的更改以修复某些bug可能会对系统的其他(难以检测)区域产生非常奇怪的影响。
显然,我们的经验集中于C ++开发环境,但是我相信您也许可以在PHP / Python下获得类似的工作。
希望这可以帮助。
《xUnit测试模式》这本书描述了一些处理命中数据库的单元测试代码的方法。我同意其他人的说法,因为这很慢,因此您不想这样做,但是IMO,您一定要这样做。模拟数据库连接以测试更高级的内容是个好主意,但请查看本书以获取有关可以与实际数据库进行交互的建议。
您有以下选择:
注入数据库。(以伪Java为例,但适用于所有OO语言)
类数据库{ 公共结果查询(字符串查询){...实际数据库在这里...} }现在,在生产环境中,您将使用普通数据库,而对于所有测试,只需注入可临时创建的模拟数据库。MockDatabase类扩展了数据库{ 公共结果查询(字符串查询){ 返回“模拟结果”; } }
类ObjectThatUsesDB { public ObjectThatUsesDB(Database db){ this.database = db; } }
User
而不是一个元组{name: "marcin", password: "blah"}
),并使用临时构造的真实对象编写所有测试,并编写一个依赖于数据库的大型测试,以确保此转换工作正常。当然,这些方法不是互相排斥的,您可以根据需要混合和匹配它们。
如果您的项目具有很高的凝聚力和松散的耦合,那么对数据库访问进行单元测试就很容易了。这样,您可以只测试每个特定类所做的事情,而不必一次测试所有内容。
例如,如果您对用户界面类进行单元测试,则编写的测试应仅尝试验证UI内的逻辑是否按预期工作,而不是验证该函数后面的业务逻辑或数据库操作。
如果要对实际的数据库访问进行单元测试,则实际上将进行更多的集成测试,因为您将依赖于网络堆栈和数据库服务器,但是您可以验证SQL代码是否按照您的要求进行操作做。
对我个人而言,单元测试的潜在力量在于,它迫使我以比没有它们时更好的方式设计应用程序。这是因为它确实帮助我摆脱了“此功能应做的一切”的思想。
抱歉,我没有用于PHP / Python的任何特定代码示例,但是如果您想查看一个.NET示例,那么我会发布一篇帖子,描述我用于进行此相同测试的技术。
我同意第一篇文章-应该将数据库访问剥离到实现接口的DAO层中。然后,您可以针对DAO层的存根实现测试您的逻辑。
为单元测试设置测试数据可能是一个挑战。
对于Java,如果使用Spring API进行单元测试,则可以在单元级别上控制事务。换句话说,您可以执行涉及数据库更新/插入/删除并回滚更改的单元测试。执行结束时,您将所有内容保留在开始执行之前的状态。对我来说,这是可以得到的。