测试核心数据应用

ma1*_*w28 5 unit-testing core-data objective-c ocmock cedar-bdd

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

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

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

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

更新:我正在使用OCHamcrest和Pivotal Lab的Cedar对核心数据进行单元测试.除了写下面的代码,我添加NSManagedObject+Additions.mUser.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
Run Code Online (Sandbox Code Playgroud)

如果你能告诉我为什么如果我不(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
Run Code Online (Sandbox Code Playgroud)

然后你获得奖励积分!:)看来我不应该投的NSNumber一个id,如果该参数应该是一个id原因NSNumberid,对不对?

Tec*_*Zen 4

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

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

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

  1. 通过删除以前存在的核心数据存储文件来开始每次测试运行,以便存储始终以已知状态开始。
  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]
Run Code Online (Sandbox Code Playgroud)

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

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

我的目的是创建一种类似于 ActiveRecord 的 find_by_ 动态查找器的方法。

...您从错误的角度处理核心数据。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 不提供一个呢?您是否更有可能错过了核心数据中的一个重要概念?