测试核心数据应用
我应该如何测试 <我添加到 NSManagedObject
的 code>findByAttribute 实例方法?
首先,我想到以编程方式创建一个独立的核心数据堆栈,如 Xcode 的核心数据实用程序教程所示。而且,在搜索该文档时,我遇到了 核心数据获取请求模板 并认为也许我不应该创建我所做的方法,而是应该创建获取请求模板,但它看起来不像entityName
可以是带有获取请求模板的变量,可以吗?我可以在 NSManagedObject
上创建一个获取请求模板以便所有子类都可以使用它吗?嗯,但是我仍然需要一个 entityName
并且我认为没有办法动态获取调用该方法的子类的名称。
无论如何,看起来一个好的解决方案是 创建内存中核心数据堆栈用于测试,独立于生产核心数据堆栈。 @Jeff Schilling 还建议创建内存中持久存储。 Chris Hanson 还创建了一个持久存储协调器来对核心数据进行单元测试。这看起来类似于 Rails 有一个单独的数据库用于测试。但是,@iamleeg 建议删除对 Core Data 的依赖。
您认为哪种方法更好?我个人更喜欢后者。
更新:我是 单元测试核心来自 OCHamcrest 和 Pivotal Lab 的 Cedar 的数据。除了编写下面的代码之外,我还向 Spec
目标添加了 NSManagedObject+Additions.m
和 User.m
。
#define HC_SHORTHAND
#import <Cedar-iPhone/SpecHelper.h>
#import <OCHamcrestIOS/OCHamcrestIOS.h>
#import "NSManagedObject+Additions.h"
#import "User.h"
SPEC_BEGIN(NSManagedObjectAdditionsSpec)
describe(@"NSManagedObject+Additions", ^{
__block NSManagedObjectContext *managedObjectContext;
beforeEach(^{
NSManagedObjectModel *managedObjectModel =
[NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *persistentStoreCoordinator =
[[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel:managedObjectModel];
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
configuration:nil URL:nil options:nil error:NULL];
managedObjectContext = [[NSManagedObjectContext alloc] init];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
[persistentStoreCoordinator release];
});
it(@"finds first object by attribute value", ^{
// Create a user with an arbitrary Facebook user ID.
NSNumber *fbId = [[NSNumber alloc] initWithInteger:514417];
[[NSEntityDescription insertNewObjectForEntityForName:@"User"
inManagedObjectContext:managedObjectContext] setFbId:fbId];
[managedObjectContext save:nil];
NSNumber *fbIdFound = [(User *)[User findByAttribute:@"fbId" value:(id)fbId
entityName:@"User"
inManagedObjectContext:managedObjectContext] fbId];
assertThatInteger([fbId integerValue], equalToInteger([fbIdFound integerValue]));
[fbId release];
});
afterEach(^{
[managedObjectContext release];
});
});
SPEC_END
如果你能告诉我为什么我不将 fbId
参数转换为 (id)
并传递给 findByAttribute
我明白了
warning: incompatible Objective-C types 'struct NSNumber *',
expected 'struct NSString *' when passing argument 2 of
'findByAttribute:value:entityName:inManagedObjectContext:' from
distinct Objective-C type
,那么您将获得奖励积分! :) 如果参数应该是 id
,我似乎不必将 NSNumber
转换为 id
因为 NSNumber
是一个id
,对吗?
How should I test the findByAttribute
instance method I added to NSManagedObject
?
At first, I thought of programmatically creating an independent Core Data stack as demonstrated by Xcode's Core Data Utility Tutorial. And, in my search for that documentation, I came across Core Data Fetch Request Templates and thought that maybe instead of creating the method I made, I should make fetch request templates, but it doesn't look like the entityName
can be variable with a fetch request template, can it? Can I create a fetch request template on NSManagedObject
so that all subclasses can use it? Hmm, but then I would still need an entityName
and I don't think there's a way to dynamically get the name of the subclass that called the method.
Anyway, it looks like a good solution is to create an in-memory Core Data stack for testing, independent from the production Core Data stack. @Jeff Schilling also recommends creating an in-memory persistent store. Chris Hanson also creates a persistent store coordinator to unit test Core Data. This seems similar to how Rails has a separate database for testing. But, @iamleeg recommends removing the Core Data dependence.
Which do you think is the better approach? I personally prefer the latter.
UPDATE: I'm unit testing Core Data with OCHamcrest and Pivotal Lab's Cedar. In addition to writing the code below, I added NSManagedObject+Additions.m
and User.m
to the Spec
target.
#define HC_SHORTHAND
#import <Cedar-iPhone/SpecHelper.h>
#import <OCHamcrestIOS/OCHamcrestIOS.h>
#import "NSManagedObject+Additions.h"
#import "User.h"
SPEC_BEGIN(NSManagedObjectAdditionsSpec)
describe(@"NSManagedObject+Additions", ^{
__block NSManagedObjectContext *managedObjectContext;
beforeEach(^{
NSManagedObjectModel *managedObjectModel =
[NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *persistentStoreCoordinator =
[[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel:managedObjectModel];
[persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
configuration:nil URL:nil options:nil error:NULL];
managedObjectContext = [[NSManagedObjectContext alloc] init];
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;
[persistentStoreCoordinator release];
});
it(@"finds first object by attribute value", ^{
// Create a user with an arbitrary Facebook user ID.
NSNumber *fbId = [[NSNumber alloc] initWithInteger:514417];
[[NSEntityDescription insertNewObjectForEntityForName:@"User"
inManagedObjectContext:managedObjectContext] setFbId:fbId];
[managedObjectContext save:nil];
NSNumber *fbIdFound = [(User *)[User findByAttribute:@"fbId" value:(id)fbId
entityName:@"User"
inManagedObjectContext:managedObjectContext] fbId];
assertThatInteger([fbId integerValue], equalToInteger([fbIdFound integerValue]));
[fbId release];
});
afterEach(^{
[managedObjectContext release];
});
});
SPEC_END
If you can tell me why if I don't cast to (id)
the fbId
argument passed to findByAttribute
I get
warning: incompatible Objective-C types 'struct NSNumber *',
expected 'struct NSString *' when passing argument 2 of
'findByAttribute:value:entityName:inManagedObjectContext:' from
distinct Objective-C type
then you get bonus points! :) It seems that I shouldn't have to cast an NSNumber
to an id
if the argument is supposed to be an id
because NSNumber
is an id
, right?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我个人的理念是,如果测试不能测试真实的东西,那么测试就不是测试,所以我对任何单独测试片段的方法持怀疑态度。尽管它在许多情况下都可以工作,尤其是在过程代码中,但在复杂的代码(例如 Core Data 对象图中的代码)中它可能会失败。
核心数据中的大多数故障点都来自于错误的数据模型,例如缺少相互关系,从而导致图表失去平衡并且出现孤立对象。测试不良图的唯一方法是创建一个已知图,然后对代码进行压力测试,看看它是否可以找到并操作图中的对象。
为了实现这种类型的测试,我执行以下操作:
每次测试时都应该绝对知道测试对象图的状态。我通常在测试中转储整个图表,并且我有方法详细转储实体和活动对象。
我通常在单独的应用程序项目设置中开发和测试应用程序的整个数据模型,除了开发数据模型之外什么都不做。只有当数据模型完全按照应用程序中的需要工作时,我才会将其移动到完整项目并开始添加控制器和界面。
由于数据模型是正确实现的模型-视图-控制器设计应用程序的实际核心,因此正确的数据模型覆盖了开发的 %50-%75。剩下的就是小菜一碟了。
在这种特殊情况下,您实际上只需要测试获取请求的谓词是否返回正确的对象。测试这一点的唯一方法是为其提供完整的测试图。
(我要指出的是,这种方法在实践中确实非常无用。它不会按属性返回任何特定对象,而只会返回具有该值属性的任意数量的对象中的任何一个。例如,如果您有一个对象图23,462 个
Person
对象,其firstName
属性值为John
,此方法将恰好返回 23,462 个任意 Person 实体。这点。我认为您正在考虑过程 SQL 术语。在处理像 Core Data 这样的对象图管理器时,这会导致混乱。)更新:
我猜测您的错误是由编译器查看 < 的使用引起的 。 code>value 在谓词中并假设它必须是一个 NSString 对象。当您删除字符串格式的对象(如
predicateWithFormat:
使用的格式)时,返回的实际值是一个 NSString 对象,其中包含该对象的description
方法的结果。因此,对于编译器来说,您的谓词实际上看起来像这样:...所以当它向后工作时,它将在
value
参数中查找 NSString,即使从技术上讲它不应该这样做。这种 id 的使用确实不是最佳实践,因为它会接受任何类,但实际上您并不总是知道实例的-description
方法返回的描述字符串是什么。正如我上面所说,你在这里遇到了一些概念问题。当你在下面的评论中说:
...您从错误的角度处理核心数据。 Active Record 主要是 SQL 的对象包装器,可以更轻松地将现有 SQL 服务器与 Ruby on Rails 集成。因此,它以过程 SQL 概念为主。
这与 Core Data 使用的方法完全相反。 Core Data 首先是一个对象图管理系统,用于创建模型-视图-控制器应用程序设计的模型层。因此,对象就是一切。例如,甚至可以拥有没有属性而只有关系的对象。这些对象也可以具有非常复杂的行为。这在 SQL 甚至 Active record 中确实不存在。
很可能有任意数量的具有完全相同属性的对象。这使得您尝试创建的方法变得毫无价值且危险,因为您永远不知道会返回哪个对象。这使得它成为一种“混乱”的方法。如果您有多个具有相同属性的对象,该方法将任意返回与提供的属性值匹配的任何单个对象。
如果要识别特定对象,则需要捕获该对象的
ManagedObjectID
,然后使用-[NSManagedObjectContext objectForID:]
检索它。保存对象后,其ManagedObjectID
是唯一的。但是,该功能通常仅在您必须引用不同商店甚至不同应用程序中的对象时使用。否则通常没有任何意义。在使用 Core Data 时,您不仅要根据对象的属性,还要根据对象的位置(即对象图中与其他对象的关系)来查找对象。
让我复制并粘贴一些非常重要的建议:Core Data 不是 SQL。实体不是表。对象不是行。列不是属性。 Core Data 是一个对象图管理系统,它可能会也可能不会持久化对象图,并且可能会也可能不会在幕后使用 SQL 来执行此操作。尝试用 SQL 术语来思考 Core Data 将会导致您完全误解 Core Data,并导致很多痛苦和浪费时间。
尝试使用您已经熟悉的 API 的设计来编写新 API 是很自然的事情,但是当新 API 与旧 API 的设计理念有很大不同时,这是一个危险的陷阱。
如果您发现自己试图在新 API 中编写旧 API 的基本功能,那么仅此一点就应该警告您,您与新 API 理念不同步。在这种情况下,您应该问为什么如果通用
findByAttribute
方法在 Core Data 中很有用,为什么 Apple 不提供一个呢?您是否更有可能错过了核心数据中的一个重要概念?My personal philosophy is that a test is not a test if it doesn't test the real thing so I look askance at any method that test fragments in isolation. Although it will work in many cases, especially in procedural code, it is likely to fail in complex code such as that found in Core Data object graphs.
Most of the points of failure in Core Data come from a bad data model e.g. missing a reciprocal relationship so that the graph comes out of balance and you have orphaned objects. The only way to test for a bad graph is to create a known graph and then stress test your code to see if it can find and manipulate the objects in the graph.
To implement this type of test I do the following:
The state of the test object graph should be absolutely know at the time of each test. I routinely dump the entire graph in testing and I have methods to dump both entities and live objects in detail.
I usually develop and test an app's entire data model in a separate app project setup to do nothing but develop the data model. Only once I have the data model working exactly as needed in the app do I move it to the full project and begin to add controllers and interface.
Since the data model is the actual core of a properly implemented Model-View-Controller design app, getting the data model correct covers %50-%75 of development. The rest is a cake walk.
In this particular case, you really only need to test that the predicate of the fetch request returns the proper objects. The only way to test that is to provide it with a full test graph.
(I would note that this method is really pretty useless in practice. It won't return any particular object by attribute but merely any one of an arbitrary number of objects that have an attribute of that value. E.g. If you have an object graph with 23,462
Person
objects with thefirstName
attribute value ofJohn
, this method will return exactly one arbitrary Person entity out fo 23,462. I fail to see the point of this. I think you are thinking in procedural SQL terms. That will lead to confusion when dealing with an object -graph manager like Core Data.)Update:
I'm going to guess that your error is caused by the complier looking at the use of
value
in the predicate and assuming it must be a NSString object. When you drop an object in a string format, like that used bypredicateWithFormat:
, the actual value returned is an NSString object containing the results of thedescription
method of the object. So, to the compiler you predicate actually looks like this:... so when it works backwards it will be looking for an NSString in the
value
parameter even though technically it shouldn't. This use of id is really not best practice because it will accept any class but you don't actually always know what the description string returned by the instance's-description
method will be.As I said above, you have some conceptual problems here. When you say in the comment below:
... you are approaching Core Data from the wrong perspective. Active Record is largely an object wrapper around SQL to make it easier to integrate existing SQL servers with Ruby on Rails. As such it is dominated by procedural SQL concepts.
That is the exact opposite approach used by Core Data. Core Data is first and foremost an object graph management system for creating the model layers of a Model-View-Controller app design. As such, the objects are everything. E.g. It is even possible to have objects without attributes, only relationships. Such objects can have very complex behaviors as well. That is something that really doesn't exist in SQL or even Active record.
It is quite possible to have an arbitrary number of objects with the exact same attributes. This makes the method you are trying to create worthless and dangerous because you will never know which object you will get back. That makes it a "chaotic" method. If you have several objects with the same attribute, the method will arbitrarily return any single object that matches attribute value provides.
If you want to identify a particular object, you need to capture the object's
ManagedObjectID
and then use-[NSManagedObjectContext objectForID:]
to retrieve it. Once an object has been saved, itsManagedObjectID
is unique.However, that feature is usually only used when you have to refer to objects in different stores or even different apps. There is usually no point otherwise. In using Core Data you are looking for objects based not only on their attributes but also their position i.e. their relationship to other objects, in the object graph.
Let me copy and paste some very important advice: Core Data is not SQL. Entities are not tables. Objects are not rows. Columns are not attributes. Core Data is an object graph management system that may or may not persist the object graph and may or may not use SQL far behind the scenes to do so. Trying to think of Core Data in SQL terms will cause you to completely misunderstand Core Data and result in much grief and wasted time.
It's natural to try and program a new API using the designs of an API you are already familiar with but it is a dangerous trap when the new API has a substantially different design philosophy from the old API.
If you find yourself trying to write a basic and fundemental function of the old API in the new one, that alone should warn you that you are not in sync with the new APIs philosophy. In this case, you should be asking why if a generic
findByAttribute
method was useful in Core Data, why didn't Apple supply one? Isn't more likely you've missed an important concept in Core Data?