如何使用数据库查询对对象进行单元测试
我听说单元测试"非常棒","非常酷"和"各式各样的好事",但是我有70%或者更多的文件涉及数据库访问(有些读和一些写),我不确定为这些文件编写单元测试。
我正在使用PHP和Python,但我认为这是一个问题,适用于大多数/所有使用数据库访问的语言。
解决方案
回答
理想情况下,对象应该是持久的无知的。例如,我们应该有一个"数据访问层",我们可以向该数据访问层发出请求,并返回对象。这样,我们可以将该部分放在单元测试之外,也可以单独进行测试。
如果对象与数据层紧密耦合,则很难进行适当的单元测试。单元测试的第一部分是"单元"。所有单元都应该能够进行隔离测试。
在我的cprojects中,我将NHibernate与完全独立的Data层一起使用。我的对象位于核心域模型中,并且可以从我的应用程序层进行访问。应用程序层与数据层和域模型层都进行对话。
应用程序层有时也称为"业务层"。
如果我们使用的是PHP,请创建一组特定的类,仅用于数据访问。确保对象不知道它们如何持久保存,并在应用程序类中连接二者。
另一种选择是使用模拟/存根。
回答
如果要对类进行单元测试,则应模拟数据库访问。毕竟,我们不想在单元测试中测试数据库。那将是一个集成测试。
将调用抽象化,然后插入一个仅返回预期数据的模拟。如果课程除了执行查询之外没有做更多的事情,那么甚至不值得测试它们,但是...
回答
我通常尝试在测试对象(和ORM,如果有)和测试数据库之间分解测试。我通过模拟数据访问调用来测试事物的对象方面,而通过测试与db的对象交互来测试事物的db方面,以我的经验,这通常是相当有限的。
在开始嘲笑数据访问部分之前,我曾经对编写单元测试感到沮丧,因此不必创建测试数据库或者即时生成测试数据。通过模拟数据,我们可以在运行时全部生成数据,并确保对象可以在已知输入下正常工作。
回答
我们可以使用模拟框架来抽象出数据库引擎。我不知道PHP / Python是否有一些功能,但是对于类型化语言(C#,Java等),有很多选择
这也取决于我们如何设计这些数据库访问代码,因为某些设计比以前的文章中提到的其他设计更容易进行单元测试。
回答
我从未在PHP中完成此操作,也从未使用过Python,但是我们想要做的是模拟对数据库的调用。为此,无论是第三方工具还是自己管理,都可以实现一些IoC,然后可以实现数据库调用程序的模拟版本,在该版本中我们可以控制该假调用的结果。
仅通过对接口进行编码即可执行IoC的简单形式。这需要在代码中进行某种面向对象的操作,因此它可能不适用于工作(我说,既然要做的就是我们提到的PHP和Python)
希望对我们有所帮助,如果我们现在没有其他要搜索的字词了。
回答
我同意第一次发布数据库的权限应该剥离到实现接口的DAO层中。然后,我们可以针对DAO层的存根实现测试逻辑。
回答
《 xUnit测试模式》一书描述了一些处理命中数据库的单元测试代码的方法。我同意其他人的说法,因为这很慢,所以我们不想这样做,但是,IMO,我们必须在某个时候这样做。模拟数据库连接以测试更高级别的内容是个好主意,但请查阅本书以获取有关可以与实际数据库进行交互的建议。
回答
如果项目具有很高的凝聚力和松散的耦合,那么对数据库访问进行单元测试就很容易了。这样,我们可以只测试每个特定类所做的事情,而不必一次测试所有内容。
例如,如果我们对用户界面类进行单元测试,则编写的测试应仅尝试验证UI内的逻辑是否按预期工作,而不是验证该函数后面的业务逻辑或者数据库操作。
如果要对实际的数据库访问进行单元测试,则实际上将进行更多的集成测试,因为我们将依赖于网络堆栈和数据库服务器,但是我们可以验证SQL代码是否按照要求进行操作做。
对我个人而言,单元测试的潜在力量在于,它迫使我以比没有它们时更好的方式设计应用程序。这是因为它确实帮助我摆脱了"此功能应做的一切"的思想。
抱歉,我没有用于PHP / Python的任何特定代码示例,但是如果我们想查看.NET示例,我会发布一篇帖子,描述我用来进行此相同测试的技术。
回答
我们有以下选择:
- 编写一个脚本,该脚本将在开始单元测试之前清除数据库,然后使用预定义的数据集填充db并运行测试。我们还可以在每次测试之前这样做,它会很慢,但是出错的可能性较小。
- 注入数据库。 (伪Java中的示例,但适用于所有OO语言)
class Database { public Result query(String query) {... real db here ...} } class MockDatabase extends Database { public Result query(String query) { return "mock result"; } } class ObjectThatUsesDB { public ObjectThatUsesDB(Database db) { this.database = db; } }
现在,在生产环境中,我们将使用普通数据库,并且对于所有测试,只需注入可临时创建的模拟数据库。
- 绝不要在大多数代码中都使用DB(无论如何这都是一种不好的做法)。创建一个"数据库"对象,而不是返回结果将返回普通对象(即将返回" User"而不是元组" {name:" marcin",密码:" blah"}`),并用即席编写所有测试构造真实对象并编写一个依赖于数据库的大型测试,以确保此转换正常进行。
当然,这些方法不是互相排斥的,我们可以根据需要混合和匹配它们。
回答
对具有数据库访问权限的对象进行单元测试的最简单方法是使用事务作用域。
例如:
[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,另一个用于随后处理单元测试,代码分析,打包等操作。
回答
我建议模拟一下我们对数据库的调用。从本质上来说,模拟是看起来像我们要在其上调用方法的对象的对象,从某种意义上说,它们具有可供调用者使用的相同属性,方法等。但是,当调用特定方法时,它没有执行程序编程要执行的任何操作,而是完全跳过了该过程,仅返回结果。该结果通常由我们提前定义。
为了设置对象以进行模拟,我们可能需要使用某种形式的控制/依赖项注入模式反转,如以下伪代码所示:
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) } }
简而言之,是一种常见的模拟场景。当然,我们仍然可能还要对实际的数据库调用进行单元测试,为此我们需要访问数据库。
回答
当我们开始对包含大量"业务逻辑" 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下获得类似的工作。
希望这可以帮助。