Tei*_*ion 146 database unit-testing
我听说单元测试"非常棒","非常酷"和"各种好东西",但70%或更多的文件涉及数据库访问(有些读取和写入)我不知道如何为这些文件编写单元测试.
我正在使用PHP和Python,但我认为这是一个适用于使用数据库访问的大多数/所有语言的问题.
Dou*_*g R 78
我建议嘲笑你对数据库的调用.模拟基本上是看起来像你试图调用方法的对象的对象,因为它们具有调用者可用的相同属性,方法等.但是,当调用特定方法时,它不会执行他们编程要执行的任何操作,而是完全跳过它,并返回结果.该结果通常由您提前定义.
为了设置对象进行模拟,您可能需要使用某种控制/依赖注入模式的反转,如下面的伪代码:
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();
}
}
Run Code Online (Sandbox Code Playgroud)
现在在单元测试中,您创建了一个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)
}
}
Run Code Online (Sandbox Code Playgroud)
简而言之,这是一种常见的模拟方案.当然,你仍然可能想要对你的实际数据库调用进行单元测试,为此你需要访问数据库.
Sea*_*ers 25
理想情况下,您的对象应该是持久无知的.例如,您应该有一个"数据访问层",您将发出请求,它将返回对象.这样,您可以将该部分从单元测试中删除,或者单独测试它们.
如果您的对象与数据层紧密耦合,则很难进行适当的单元测试.单元测试的第一部分是"单元".所有单元都应该能够单独进行测试.
在我的c#项目中,我使用NHibernate与完全独立的数据层.我的对象存在于核心域模型中,可以从我的应用层访问.应用程序层与数据层和域模型层进行通信.
应用程序层有时也称为"业务层".
如果您使用的是PHP,请为仅限数据访问创建一组特定的类.确保您的对象不知道它们是如何持久化的,并在应用程序类中连接两者.
另一种选择是使用模拟/存根.
小智 11
使用数据库访问对对象进行单元测试的最简单方法是使用事务范围.
例如:
[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);
}
}
Run Code Online (Sandbox Code Playgroud)
这将恢复数据库的状态,基本上类似于事务回滚,因此您可以根据需要多次运行测试而不会产生任何副作用.我们在大型项目中成功使用了这种方法.我们的构建需要一段时间才能运行(15分钟),但是进行1800次单元测试并不可怕.此外,如果构建时间是一个问题,您可以将构建过程更改为具有多个构建,一个用于构建src,另一个在之后启动,处理单元测试,代码分析,打包等...
Ala*_*lan 10
当我们开始研究包含大量"业务逻辑"sql操作的中间层进程的单元测试时,我或许可以让您体验一下我们的体验.
我们首先创建了一个抽象层,允许我们"插入"任何合理的数据库连接(在我们的例子中,我们只支持单个ODBC类型的连接).
一旦这个到位,我们就可以在我们的代码中做这样的事情(我们在C++中工作,但我相信你明白了):
GetDatabase().ExecuteSQL("INSERT INTO foo(blah,blah)")
在正常运行时,GetDatabase()将返回一个对象,该对象通过ODBC直接向数据库提供所有sql(包括查询).
然后我们开始查看内存数据库 - 最好的方法似乎是SQLite.(http://www.sqlite.org/index.html).它的设置和使用非常简单,并允许我们使用子类和覆盖GetDatabase()将sql转发到为每次执行的测试创建和销毁的内存数据库.
我们还处于早期阶段,但到目前为止看起来还不错,但是我们必须确保创建所需的表并用测试数据填充它们 - 但是我们通过创建来减少工作量一组通用的辅助函数,可以为我们做很多事情.
总的来说,它对我们的TDD过程有很大的帮助,因为修复某些错误看起来非常无害的变化会对系统的其他(难以检测)区域产生相当奇怪的影响 - 由于sql /数据库的本质.
显然,我们的经验集中在C++开发环境中,但是我相信你可能会在PHP/Python下得到类似的东西.
希望这可以帮助.
如果要对类进行单元测试,则应该模拟数据库访问.毕竟,您不希望在单元测试中测试数据库.那将是一次整合测试.
抽象调用,然后插入一个只返回预期数据的模拟.如果你的类不只是执行查询,它甚至可能不值得测试它们,尽管......
我通常尝试在测试对象(和 ORM,如果有的话)和测试数据库之间分解我的测试。我通过模拟数据访问调用来测试事物的对象端,而我通过测试对象与 db 的交互来测试事物的 db 端,根据我的经验,这通常相当有限。
我曾经对编写单元测试感到沮丧,直到我开始模拟数据访问部分,所以我不必创建测试数据库或即时生成测试数据。通过模拟数据,您可以在运行时生成所有数据,并确保您的对象在已知输入下正常工作。
您有以下选择:
注入数据库。(伪 Java 中的示例,但适用于所有 OO 语言)
类数据库{
公共结果查询(字符串查询){...真实数据库在这里...}
}
类 MockDatabase 扩展数据库 {
公共结果查询(字符串查询){
返回“模拟结果”;
}
}
类 ObjectThatUsesDB {
公共 ObjectThatUsesDB(数据库 db){
这个.数据库 = db;
}
}
现在在生产中,您使用普通数据库,对于所有测试,您只需注入可以临时创建的模拟数据库。User不是元组{name: "marcin", password: "blah"})使用临时构造的真实对象编写所有测试,并编写一个依赖于确保此转换的数据库的大型测试工作正常。当然,这些方法并不相互排斥,您可以根据需要混合搭配它们。