【问题标题】:Test Core Data Application测试核心数据应用
【发布时间】:2011-03-18 03:23:14
【问题描述】:

我应该如何测试findByAttribute instance method I added to NSManagedObject

一开始我想到了programmatically creating an independent Core Data stack as demonstrated by Xcode's Core Data Utility Tutorial。而且,在我搜索该文档时,我遇到了Core Data Fetch Request Templates,并认为也许我应该创建获取请求模板而不是创建我所做的方法,但它看起来不像 entityName 可以使用获取请求模板,可以吗?我可以在NSManagedObject 上创建一个获取请求模板,以便所有子类都可以使用它吗?嗯,但是我仍然需要entityName,而且我认为没有办法动态获取调用该方法的子类的名称。

无论如何,create an in-memory Core Data stack for testing 看起来是一个不错的解决方案,独立于生产核心数据堆栈。 @Jeff Schilling also recommends creating an in-memory persistent storeChris Hanson also creates a persistent store coordinator to unit test Core Data。这似乎类似于 Rails 有一个单独的数据库进行测试。但是,@iamleeg recommends removing the Core Data dependence

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

更新:我是unit testing Core Data with OCHamcrest and Pivotal Lab's Cedar。除了编写下面的代码之外,我还在Spec 目标中添加了NSManagedObject+Additions.mUser.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

如果你能告诉我为什么如果我不cast to (id) the fbId argument passed to 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,因为NSNumberid,对吧?

【问题讨论】:

  • +1 一个写得很好的问题。感谢您花时间撰写它。
  • 第 47 行是哪一行?您的行号未显示在帖子的代码中。 ;-)
  • 这是我打电话给findByAttribute的地方。我取出“第 47 行”并用该描述替换它。

标签: objective-c unit-testing core-data ocmock cedar-bdd


【解决方案1】:

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

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

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

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

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

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

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

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

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

更新:

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

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

...所以当它向后工作时,它会在value 参数中寻找一个NSString,即使从技术上讲它不应该。这种使用 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 中的一个重要概念?

【讨论】:

  • 感谢您的出色回答!我遵循了您的建议和问题中的一些链接来找出解决方案,并添加到我的问题中。此外,findByAttribute 主要用于通过唯一属性查找托管对象。我的意图是制作一种类似于ActiveRecord's find_by_ dynamic finder 的方法。也许,我应该给它一个排序描述符,但是我会按什么排序呢?也许我最终也会实现find_all_by_,然后find_by 将只是find_all_by_ 的包装器,limit = 1。
猜你喜欢
  • 1970-01-01
  • 2011-06-24
  • 2014-08-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多