Тестовое приложение для обработки основных данных - PullRequest
5 голосов
/ 18 марта 2011

Как мне протестировать метод экземпляра findByAttribute, который я добавил к NSManagedObject?

Сначала я подумал о программном создании независимого стека базовых данных, как продемонстрированопо учебному пособию по основным данным Xcode .И в процессе поиска этой документации я наткнулся на Core Data Fetch Request Templates и подумал, что, возможно, вместо создания метода, который я сделал, я должен создать шаблоны запросов на выборку, но это не похоже наentityName может быть переменной с шаблоном запроса выборки, не так ли?Могу ли я создать шаблон запроса на выборку в NSManagedObject, чтобы его могли использовать все подклассы?Хм, но тогда мне все еще нужен entityName, и я не думаю, что есть способ динамически получить имя подкласса, который вызвал метод.

В любом случае, похоже, что хорошее решение - это создание стека базовых данных в памяти для тестирования , независимого от производственного стека базовых данных. @ Джефф Шиллинг также рекомендует создать постоянное хранилище в памяти . Крис Хэнсон также создает постоянный координатор хранилища для модульного тестирования Core Data .Это похоже на то, как у Rails есть отдельная база данных для тестирования.Но @ iamleeg рекомендует удалить зависимость от базовых данных .

Как вы думаете, какой подход лучше?Я лично предпочитаю последнее.

ОБНОВЛЕНИЕ: я тестирую базовые данные с помощью OCHamcrest и Cedar из Pivotal Lab.В дополнение к написанию кода ниже, я добавил NSManagedObject+Additions.m и User.m к цели Spec.

#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) аргумент fbId, переданный 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

тогда вы получите бонусные очки!:) Кажется, мне не нужно приводить NSNumber к id, если аргумент должен быть id, потому что NSNumber это id, верно?

1 Ответ

4 голосов
/ 18 марта 2011

Моя личная философия заключается в том, что тест не является тестом, если он не тестирует реальную вещь, поэтому я косо смотрю на любой метод, который тестирует фрагменты изолированно. Хотя он будет работать во многих случаях, особенно в процедурном коде, он может дать сбой в сложном коде, например, в графах объектов Core Data.

Большинство точек сбоя в базовых данных происходят из-за неверной модели данных, например отсутствует взаимное отношение, так что график выходит из равновесия, и у вас появляются осиротевшие объекты. Единственный способ проверить плохой граф - это создать известный граф, а затем провести стресс-тестирование вашего кода, чтобы посмотреть, сможет ли он найти объекты на графике и манипулировать ими.

Чтобы реализовать этот тип теста, я делаю следующее:

  1. Запускайте каждый тестовый запуск, удаляя ранее существующий файл хранилища Core Data, чтобы хранилище всегда начиналось в известном состоянии.
  2. Предоставьте новое хранилище для каждого прогона, желательно, каждый раз генерируя его в коде, но вы можете просто поменять копию файла хранилища перед каждым прогоном. Я предпочитаю первый метод, потому что он на самом деле легче в долгосрочной перспективе.
  3. Убедитесь, что данные испытаний содержат относительно экстремальные примеры, например, длинные имена, строки с символами мусора, очень большие и очень маленькие цифры и т. д.

Состояние графа тестового объекта должно быть абсолютно известно во время каждого теста. Я регулярно выкидываю весь граф в тестирование, и у меня есть методы для подробного вывода как сущностей, так и живых объектов.

Обычно я разрабатываю и тестирую всю модель данных приложения в отдельной настройке проекта приложения, чтобы ничего не делать, кроме разработки модели данных. Только после того, как модель данных работает точно так, как необходимо в приложении, я перемещаю ее в полный проект и начинаю добавлять контроллеры и интерфейс.

Поскольку модель данных является фактическим ядром правильно реализованного приложения для проектирования Model-View-Controller, правильная модель данных покрывает 50-75% разработки. Остальное - прогулка по пирогу.

В этом конкретном случае вам действительно нужно только проверить, что предикат запроса на выборку возвращает правильные объекты. Единственный способ проверить это - предоставить ему полный график тестирования.

(Я хотел бы отметить, что этот метод на самом деле довольно бесполезен на практике. Он не будет возвращать какой-либо конкретный объект по атрибуту, а просто любой из произвольного числа объектов, имеющих атрибут этого значения. Например, если у вас есть Граф объектов с 23 462 Person объектами со значением атрибута firstName, равным John, этот метод вернет ровно одну произвольную сущность Person из 23 462. Я не вижу смысла в этом. Я думаю, что вы думаете в процедурном SQL условия. Это может привести к путанице при работе с менеджером объект-графа, таким как Core Data.)

Обновление:

Я собираюсь предположить, что ваша ошибка вызвана тем, что компилятор рассматривает использование 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 - это, прежде всего, система управления графами объектов для создания слоев модели в проекте приложения Model-View-Controller. Таким образом, объекты являются всем. Например. Можно даже иметь объекты без атрибутов, только отношения. Такие объекты также могут иметь очень сложное поведение. Это то, чего на самом деле не существует в SQL или даже в активной записи.

Вполне возможно иметь произвольное количество объектов с одинаковыми атрибутами. Это делает метод, который вы пытаетесь создать, бесполезным и опасным, потому что вы никогда не будете знать, какой объект вы получите. Это делает его "хаотичным" методом. Если у вас есть несколько объектов с одним и тем же атрибутом, метод будет произвольно возвращать любой отдельный объект, который соответствует значению атрибута.

Если вы хотите идентифицировать конкретный объект, вам нужно захватить ManagedObjectID объекта, а затем использовать -[NSManagedObjectContext objectForID:], чтобы получить его. После сохранения объекта ManagedObjectID становится уникальным.

Однако эта функция обычно используется только тогда, когда вам приходится ссылаться на объекты в разных магазинах или даже в разных приложениях. Обычно нет смысла иначе. При использовании Базовых данных вы ищете объекты, основанные не только на их атрибутах, но и на их положении, то есть их отношении к другим объектам в графе объектов.

Позвольте мне скопировать и вставить несколько очень важных советов: Базовые данные - это не SQL. Сущности не являются таблицами. Объекты не являются строками. Столбцы не являются атрибутами. Базовые данные - это система управления графом объектов, которая может сохранять или не сохранять объектный граф, а может и не использовать SQL для этого далеко за кулисами. Попытка представить базовые данные в терминах SQL приведет к тому, что вы полностью неправильно поймете базовые данные и приведет к большим трудностям и потерянному времени.

Естественно попытаться запрограммировать новый API, используя конструкции API, с которыми вы уже знакомы, но это опасная ловушка, когда новый API имеет существенно отличную философию дизайна от старого API.

Если вы пытаетесь написать базовую и основную функцию старого API в новом, это само по себе должно предупредить вас, что вы не синхронизированы с новой философией API. В этом случае вы должны спросить, почему, если универсальный метод findByAttribute был полезен в Core Data, почему Apple не предоставил его? Не более ли вероятно, что вы пропустили важную концепцию в Core Data?

...