5

我应该如何测试findByAttribute我添加到的实例方法NSManagedObject

起初,我想以编程方式创建一个独立的 Core Data 堆栈,如 Xcode 的 Core Data Utility Tutorial 所示。而且,在我搜索该文档时,我遇到了Core Data Fetch Request Templates并认为可能不是创建我创建的方法,而是我应该制作 fetch 请求模板,但它看起来不像entityName可以通过 fetch 变量请求模板,可以吗?我可以创建一个获取请求模板,NSManagedObject以便所有子类都可以使用它吗?嗯,但是我仍然需要一个entityName并且我认为没有办法动态获取调用该方法的子类的名称。

无论如何,看起来一个很好的解决方案是创建一个用于测试的内存中核心数据堆栈,独立于生产核心数据堆栈。@Jeff Schilling 还建议创建一个内存持久存储Chris Hanson 还创建了一个持久存储协调器来对 Core Data 进行单元测试。这似乎类似于 Rails 有一个单独的数据库进行测试。但是,@iamleeg 建议删除 Core Data 依赖项

您认为哪种方法更好?我个人更喜欢后者。

更新:我正在使用 OCHamcrest 和 Pivotal Lab 的 Cedar 对核心数据进行单元测试。除了编写下面的代码之外,我还在目标中添加了NSManagedObject+Additions.m和。User.mSpec

#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

如果你能告诉我为什么如果我不强制(id)fbIdfindByAttribute传递给我得到论点

warning: incompatible Objective-C types 'struct NSNumber *',
expected 'struct NSString *' when passing argument 2 of
'findByAttribute:value:entityName:inManagedObjectContext:' from
distinct Objective-C type

然后你得到奖励积分!:)如果论点应该是an ,我似乎不必将 anNSNumber转换为 an因为是 an ,对吗?ididNSNumberid

4

1 回答 1

4

我个人的理念是,如果不测试真实的东西,测试就不是测试,所以我对任何单独测试片段的方法都持怀疑态度。尽管它在许多情况下都可以工作,尤其是在过程代码中,但它很可能在复杂代码中失败,例如在 Core Data 对象图中发现的代码。

Core Data 中的大多数故障点都来自糟糕的数据模型,例如缺少互惠关系,从而导致图表失去平衡并且您拥有孤立的对象。测试坏图的唯一方法是创建一个已知图,然后对代码进行压力测试,看看它是否可以找到并操作图中的对象。

为了实现这种类型的测试,我执行以下操作:

  1. 通过删除先前存在的 Core Data 存储文件开始每次测试运行,以便存储始终以已知状态开始。
  2. 为每次运行提供一个新的存储,最好每次在代码中生成它,但您可以在每次运行之前交换存储文件的副本。我更喜欢前一种方法,因为从长远来看它实际上更容易。
  3. 确保测试数据包含相对极端的示例,例如长名称、带有垃圾字符的字符串、非常大和非常小的数字等。

测试对象图的状态应该在每次测试时绝对知道。我经常在测试中转储整个图表,并且我有方法可以详细转储实体和活动对象。

我通常在单独的应用程序项目设置中开发和测试应用程序的整个数据模型,除了开发数据模型之外什么都不做。只有当数据模型在应用程序中完全按照需要工作时,我才会将其移动到整个项目并开始添加控制器和接口。

由于数据模型是正确实现的模型-视图-控制器设计应用程序的实际核心,因此正确的数据模型涵盖了 %50-%75 的开发。剩下的就是一个蛋糕散步。

在这种特殊情况下,您实际上只需要测试获取请求的谓词是否返回正确的对象。测试它的唯一方法是为其提供完整的测试图。

(我会注意到,这种方法在实践中真的非常没用。它不会按属性返回任何特定对象,而只会返回具有该值属性的任意数量的对象中的任何一个。例如,如果您有一个对象图23,462 个属性值为 的Person对象,此方法将在 23,462 中返回一个任意的 Person 实体。我看不到这一点。我想你是在用过程 SQL 术语思考。在处理对象时会导致混淆- 像 Core Data 这样的图形管理器。)firstNameJohn

更新:

我猜你的错误是由编译器查看value谓词中的使用并假设它必须是一个 NSString 对象引起的。当你删除一个字符串格式的对象时,就像使用的那样predicateWithFormat:,返回的实际值是一个 NSString 对象,其中包含该对象的description方法的结果。因此,对于编译器,您的谓词实际上如下所示:

[NSPredicate predicateWithFormat:@"%K == %@", (NSString *)attribute, (NSString *)value]

...所以当它向后工作时,它会在参数中寻找一个 NSString ,value即使从技术上讲它不应该。这种使用 id 确实不是最佳实践,因为它可以接受任何类,但您实际上并不总是知道实例-description方法返回的描述字符串是什么。

正如我上面所说,你在这里有一些概念上的问题。当你在下面的评论中说:

我的意图是制作一种类似于 ActiveRecord 的 find_by_ 动态查找器的方法。

...您从错误的角度接近核心数据。Active Record 在很大程度上是一个围绕 SQL 的对象包装器,可以更轻松地将现有 SQL 服务器与 Ruby on Rails 集成。因此,它由过程 SQL 概念主导。

这与 Core Data 使用的方法完全相反。Core Data 首先是一个对象图管理系统,用于创建模型-视图-控制器应用程序设计的模型层。因此,对象就是一切。例如,甚至可以有没有属性的对象,只有关系。此类对象也可以具有非常复杂的行为。那是在 SQL 甚至 Active 记录中真正不存在的东西。

很可能有任意数量的具有完全相同属性的对象。这使您尝试创建的方法变得毫无价值和危险,因为您永远不知道您将返回哪个对象。这使它成为一种“混乱”的方法。如果您有多个具有相同属性的对象,则该方法将任意返回与提供的属性值匹配的任何单个对象。

如果你想识别一个特定的对象,你需要捕获这个对象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 不提供呢?您是否更有可能错过了 Core Data 中的一个重要概念?

于 2011-03-18T15:29:10.593 回答