Как имитировать службу определения местоположения с помощью KIF-фреймворка

Я использую структуру KIF (http://github.com/kif-framework/KIF) для Тесты пользовательского интерфейса, и мне нужно смоделировать службу определения местоположения.

Проблема заключается в том, что служба определения местоположения запускается ДО вызова метода KIF -beforeAll. Так что поздно издеваться.

Мы ценим любые предложения.


person n0_quarter    schedule 21.03.2014    source источник
comment
Можете ли вы предоставить какой-либо пример кода для воспроизведения проблемы?   -  person bllakjakk    schedule 16.02.2015


Ответы (2)


В моей цели KIF у меня есть BaseKIFSearchTestCase : KIFTestCase, где я перезаписываю startUpdatingLocation CLLocationManager в категории.

Обратите внимание, что это единственная перезапись категории, которую я когда-либо делал, поскольку в целом это не очень хорошая идея. но в тестовой задаче я могу это принять.

#import <CoreLocation/CoreLocation.h>

#ifdef TARGET_IPHONE_SIMULATOR


@interface CLLocationManager (Simulator)
@end

@implementation CLLocationManager (Simulator)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

-(void)startUpdatingLocation 
{
    CLLocation *fakeLocation = [[CLLocation alloc] initWithLatitude:41.0096334 longitude:28.9651646];
    [self.delegate locationManager:self didUpdateLocations:@[fakeLocation]];
}
#pragma clang diagnostic pop

@end
#endif // TARGET_IPHONE_SIMULATOR



#import "BaseKIFSearchTestCase.h"

@interface BaseKIFSearchTestCase ()

@end

@implementation BaseKIFSearchTestCase
 //...

@end

Более чистым было бы иметь подкласс CLLocationManager в вашей цели приложения и другой подкласс с тем же именем в вашей тестовой цели, которые отправляют поддельное местоположение, как показано выше. Но возможно ли это, зависит от того, как настроена ваша тестовая цель, поскольку на самом деле она должна быть целью приложения, которую использует Calabash.


Еще один способ:

  • в вашем проекте создайте другую конфигурацию "Тестирование", клонирование "Отладка"

  • добавьте Preprocessor Macro TESTING=1 в эту конфигурацию.

  • Подкласс CLLocationManager

  • используйте этот подкласс, где вы бы использовали CLLocaltionManger

  • условно скомпилировать этот класс

    #import "GELocationManager.h"
    
    @implementation GELocationManager
    -(void)startUpdatingLocation
    {
    
    #if TESTING==1
    #warning Testmode
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            CLLocation *fakeLocation = [[CLLocation alloc] initWithLatitude:41.0096334 longitude:28.9651646];
            [self.delegate locationManager:self didUpdateLocations:@[fakeLocation]];
        });
    
    #else
        [super startUpdatingLocation];
    #endif
    
    }
    @end
    
  • в вашей схеме тестовых целей выберите новую конфигурацию


И еще вариант:

введите здесь описание изображения

Вероятно, лучший: код не нужно менять.

person vikingosegundo    schedule 18.02.2015
comment
Ваш третий вариант, который кажется таким простым, кажется, не работает для меня. Я выбираю Сан-Франциско в качестве своего местоположения, но когда я запускаю тесты, я получаю предупреждение о неудачном вызове местоположения. - person Zeek Aran; 13.03.2018

Как обычно, несколько способов сделать это. Суть не в том, чтобы пытаться сымитировать существующую службу определения местоположения, а создать совершенно другой макет, к которому вы можете получить доступ во время выполнения. Первый метод, который я собираюсь описать, — это создание собственного крошечного контейнера внедрения зависимостей. Второй метод предназначен для доступа к синглтонам, к которым у вас обычно нет доступа.

1) Рефакторинг вашего кода, чтобы он не использовал LocationService напрямую. Вместо этого инкапсулируйте его в держатель (может быть простой одноэлементный класс). Затем сделайте свой держатель тест-осведомленным. Как это работает, у вас есть что-то вроде LocationServiceHolder, которое имеет:

// Do some init for your self.realService and make this holder
// a real singleton.

+ (LocationService*) locationService {
  return useMock ? self.mockService : self.realService;
}

- (void)useMock:(BOOL)useMock {
  self.useMock = useMock;
}

- (void)setMock:(LocationService*)mockService {
  self.mockService = mockService;
}

Затем всякий раз, когда вам нужна служба определения местоположения, вы звоните

[[LocationServiceHolder sharedService] locationService];  

Итак, когда вы тестируете, вы можете сделать что-то вроде:

- (void)beforeAll {
  id mock = OCClassMock([LocationService class]);
  [[LocationServiceHolder sharedService] useMock:YES]];
  [[LocationServiceHolder sharedService] setMock:mock]];
}

- (void)afterAll {
  [[LocationServiceHolder sharedService] useMock:NO]];
  [[LocationServiceHolder sharedService] setMock:nil]];      
}

Конечно, вы можете сделать это в beforeEach и переписать семантику, чтобы она была немного лучше, чем в базовой версии, которую я здесь показываю.

2) Если вы используете сторонний LocationService, который является синглтоном, который вы не можете изменить, это немного сложнее, но все же выполнимо. Хитрость здесь заключается в том, чтобы использовать категорию для переопределения существующих одноэлементных методов и предоставления макета, а не обычного одноэлементного. Хитрость внутри трюка заключается в том, чтобы иметь возможность отправить сообщение обратно в исходный синглтон, если макет не существует.

Допустим, у вас есть синглтон под названием ThirdPartyService. Вот MockThirdPartyService.h:

static ThirdPartyService *mockThirdPartyService;

@interface ThirdPartyService (Testing)

+ (id)sharedInstance;
+ (void)setSharedInstance:(ThirdPartyService*)instance;
+ (id)mockInstance;

@end

А вот MockThirdPartyService.m:

#import "MockThirdPartyService.h"
#import "NSObject+SupersequentImplementation.h"

// Stubbing out ThirdPartyService singleton
@implementation ThirdPartyService (Testing)

+(id)sharedInstance {
    if ([self mockInstance] != nil) {
        return [self mockInstance];
    }
    // What the hell is going on here? See http://www.cocoawithlove.com/2008/03/supersequent-implementation.html
    IMP superSequentImp = [self getImplementationOf:_cmd after:impOfCallingMethod(self, _cmd)];
    id result = ((id(*)(id, SEL))superSequentImp)(self, _cmd);
    return result;
}

+ (void)setSharedInstance:(ThirdPartyService *)instance {
    mockThirdPartyService = instance;
}

+ (id)mockInstance {
    return mockThirdPartyService;
}

@end

Чтобы использовать, вы должны сделать что-то вроде:

#include "MockThirdPartyService.h"

...

id mock = OCClassMock([ThirdPartyService class]);
[ThirdPartyService setSharedInstance:mock];

// set up your mock and do your testing here

// Once you're done, clean up.
[ThirdPartyService setSharedInstance:nil];
// Now your singleton is no longer mocked and additional tests that
// don't depend on mock behavior can continue running.

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

Вывод: DI - это хорошо. Люди жалуются на необходимость рефакторинга и изменения кода только для тестирования, но тестирование, вероятно, является наиболее важной частью качественной разработки программного обеспечения, а DI + ApplicationContext значительно упрощает задачу. Мы используем фреймворк Typhoon, но даже создание собственного шаблона DI + ApplicationContext того стоит, если вы выполняете какой-либо уровень тестирования.

person plluke    schedule 18.02.2015
comment
Хотя ваш вывод о рефакторинге кода для теста верен, вы не показываете DI в своем ответе, так как переключаете внутреннее поведение службы и не передаете объект тестовой службы. DI и синглтоны никогда не сочетаются друг с другом. - person vikingosegundo; 19.02.2015
comment
@vikingosegundo, пожалуйста, перечитайте мой ответ. Я специально говорю, что только первый метод - это создание вашего собственного крошечного DI, а второй - для насмешки над синглтонами, которые вы не можете контролировать, что я никогда не утверждал, что это DI. DI и синглтоны прекрасно сочетаются друг с другом. DI — это то, что позволяет вам поддерживать любую область действия для объекта (одиночный, слабый синглтон и т. д.) и при этом поддерживать тестируемость. - person plluke; 19.02.2015
comment
никогда раньше не слышал термина крошечный DI. Должно быть, пропустил этот урок. - person vikingosegundo; 19.02.2015
comment
@vikingosegundo Если бы я ответил, просто используйте DI, я не думаю, что это было бы полезно. И если бы у OP был DI, это не было бы проблемой. Контекст вопроса подразумевал отсутствие DI и осведомленности о DI, поэтому я привел один базовый пример реализации системы DI без большого количества функций, но которая дает вам минимальный уровень абстракции. И второй, когда что-то выходит из-под вашего контроля. Не знаю, куда вы клоните с замечаниями, как будто пропустили это в классе. Критикуйте мой ответ напрямую. - person plluke; 19.02.2015
comment
хорошо, вот мои критики: нет причин, по которым вообще должен быть задействован синглтон. Но именно это и подразумевает ваш ответ. Почему? Я могу создать экземпляр CLLocationManger? Я просто могу передать его туда, где мне это нужно. Мне не нужен одноэлементный сервис вокруг него, который портит мои зависимости. Мне просто нужна одна служба, которую я передаю любому объекту, который в ней нуждается. - person vikingosegundo; 19.02.2015
comment
Даже в DI контекст приложения, как правило, является одноэлементным (или, по крайней мере, объектом долгосрочного жизненного цикла), чтобы обслуживать все потребности в зависимостях. Для CLLocationManager конкретно нет, в этом нет необходимости, но шаблон, который я показал, был общим и не полагался на семантику CLLocationManager. ОП также не сказал, что он конкретно использует CLLocationManager, поэтому я показал пример self.realService. Передача объектов - это нормально, но наличие контекста приложения, в котором хранятся ваши зависимости, позволяет вам исправить контекст вашего приложения и избежать написания категории, которую вы указали в своем ответе. - person plluke; 19.02.2015
comment
@vikingosegundo Я понимаю, что вы немного догматичны в отношении DI и синглетонов, и это нормально. Но мой ответ не для тех, кто разбирается в этом, поскольку OP, похоже, не знает, и он также не предназначен для идеального DI. Мой ответ пытается познакомить OP с идеей инверсии управления, в то же время не решая только один случай, характерный для CLLocationManager. Я не думаю, что ваш ответ об использовании категории для переопределения одного метода очень гибкий, но у него есть свое применение, поэтому я не стал разглагольствовать о вашем ответе. Пока ОП считает это полезным. Каждому свое. - person plluke; 19.02.2015
comment
Я не разглагольствую о вашем ответе. Вы хотели критиков, я предоставил. и я не догматик. Я просто работаю в больших командах и знаю ценность использования здоровой архитектуры. Если класс использует синглтоны внутри, это рано или поздно создаст головную боль. Вы рекламируете DI и использование Singleton в одном и том же посте. для меня это звучит очень странно, как вы говорите: не ленитесь, реорганизуйте свой код, но используйте синглетоны, так как все остальное слишком много работы и догматизма. - person vikingosegundo; 19.02.2015
comment
единственный способ обработки синглетонов: self.endpointController = [[VSAPIEndpointController alloc] initWithUserDefaults:[NSUserDefaults standardUserDefaults] liveConfigController:self.liveConfigController];. По двум причинам: при тестировании я могу передавать макеты пользовательских значений по умолчанию, и любой из моих со-кодеров будет знать из подписи, что пользовательские значения по умолчанию имеют значение и влияют на этот класс. - person vikingosegundo; 19.02.2015
comment
Спасибо за Ваш ответ. Некоторые из них будут полезны, но я думаю, что это не показывает, как решить исходную проблему, когда beforeAll вызывается после инициализации UIViewController и, возможно, уже обращается к CLLocationManager или чему-то еще. - person MaciejGórski; 20.02.2015