Stubbing/mocking upservices для приложения iOS
Я работаю над iOS-приложением, основной целью которого является общение с набором удаленных веб-сервисов. Для тестирования интеграции я хотел бы иметь возможность запускать свое приложение против каких-то поддельных веб-сервисов, которые имеют предсказуемый результат.
До сих пор я видел два предложения:
- Создайте веб-сервер, который обслуживает статические результаты для клиента (например здесь).
- Реализовать другой код обмена webservice, который на основе флага времени компиляции вызовет либо веб-службы, либо код, который будет загружать ответы из локального файла (example и другой).
Мне любопытно, что сообщество думает о каждом из этих подходов и есть ли там какие-либо инструменты для поддержки этого рабочего процесса.
Обновить. Позвольте мне указать конкретный пример. У меня есть форма входа в систему, в которой указаны имя пользователя и пароль. Я бы хотел проверить два условия:
Итак, мне нужен код для проверки параметра имени пользователя и выдача соответствующего ответа на меня. Надеюсь, что вся логика, которая мне нужна в "поддельном веб-сервисе". Как мне это сделать чисто?
Ответы
Ответ 1
Что касается опции 1, я в прошлом использовал это с помощью CocoaHTTPServer и встраивал сервер непосредственно в тест OCUnit:
https://github.com/robbiehanson/CocoaHTTPServer
Я использую код для использования этого в unit test здесь:
https://github.com/quellish/UnitTestHTTPServer
В конце концов, HTTP - это просто запрос/ответ.
Издеваться над веб-сервисом, помогая создавать макетный HTTP-сервер или создавая макет веб-сервиса в коде, будет примерно такой же объем работы. Если у вас есть тестовые коды X-кода, у вас есть, по крайней мере, X-коды кода для обработки вашего макета.
Для варианта 2, чтобы издеваться над веб-службой, вы не будете общаться с веб-службой, вместо этого вы будете использовать макет-объект, который имеет известные ответы.
[MyCoolWebService performLogin:username withPassword:password]
станет в вашем тесте
[MyMockWebService performLogin:username withPassword:password]
Ключевым моментом является то, что MyCoolWebService и MyMockWebService используют один и тот же контракт (в objective-c, это будет протокол). У OCMock есть много документации, чтобы вы начали.
Однако для теста интеграции вы должны тестировать реальный веб-сервис, например, среду QA/промежуточного уровня. То, что вы на самом деле описываете, больше похоже на функциональное тестирование, чем на тестирование интеграции.
Ответ 2
Я предлагаю использовать Nocilla. Nocilla - это библиотека для обнуления HTTP-запросов с помощью простой DSL.
Скажем, что вы хотите вернуть 404 с google.com. Все, что вам нужно сделать, это:
stubRequest(@"GET", "http://www.google.com").andReturn(404); // Yes, it ObjC
После этого любой HTTP to google.com вернет 404.
Более полный пример, в котором вы хотите сопоставить POST с определенным телом и заголовками и вернуть законченный ответ:
stubRequest(@"POST", @"https://api.example.com/dogs.json").
withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}).
withBody(@"{\"name\":\"foo\"}").
andReturn(201).
withHeaders(@{@"Content-Type": @"application/json"}).
withBody(@"{\"ok\":true}");
Вы можете сопоставить любой запрос и подделать любой ответ. Проверьте README для получения более подробной информации.
Преимущества использования Nocilla над другими решениями:
- Это быстро. Нет HTTP-серверов для запуска. Ваши тесты будут выполняться очень быстро.
- Никаких сумасшедших зависимостей для управления. Кроме того, вы можете использовать CocoaPods.
- Он хорошо протестирован.
- Отличный DSL, который сделает ваш код очень легким для понимания и поддержки.
Основное ограничение заключается в том, что он работает только с инфраструктурами HTTP, созданными поверх NSURLConnection, такими как AFNetworking, MKNetworkKit или plain NSURLConnection.
Надеюсь, это поможет. Если вам нужно что-нибудь еще, я здесь, чтобы помочь.
Ответ 3
Я предполагаю, что вы используете Objective-C. Для Objective-C OCMock широко используется для издевательского/модульного тестирования (ваш второй вариант).
Я использовал OCMock в последний раз больше, чем год назад, но насколько я помню, это полноценная фальшивая каркас и может делать все, что описано ниже.
Одна важная вещь в mocks заключается в том, что вы можете использовать столько или меньше фактической функциональности своих объектов. Вы можете создать "пустой" макет (который будет иметь все методы - ваш объект, но ничего не сделает) и переопределить только те методы, которые вам нужны в вашем тесте. Обычно это делается при тестировании других объектов, которые полагаются на макет.
Или вы можете создать макет, который будет действовать как ваш реальный объект, и выровнять некоторые методы, которые вы не хотите тестировать на этом уровне (например, - методы, которые фактически обращаются к базе данных, требуют сетевого подключения и т.д.), Обычно это делается, когда вы тестируете сам издевавшийся объект.
Важно понимать, что вы не создаете mocks раз и навсегда. Каждый тест может создавать mocks для одних и тех же объектов заново на основе того, что тестируется.
Еще одна важная вещь в mocks заключается в том, что вы можете "записывать" сценарий (последовательности вызовов) и ваши "ожидания" о них (какие методы за кулисами следует вызывать, с какими параметрами и в каком порядке) повторить "сценарий - тест потерпит неудачу, если ожидания не будут выполнены. Это основное различие между классическим и mockist TDD. Он имеет свои плюсы и минусы (см. Статью Мартина Фаулера).
Теперь рассмотрим ваш конкретный пример (я буду использовать псевдосинтакс, который больше похож на С++ или Java, а не Objective C):
Предположим, что у вас есть объект класса LoginForm
, который представляет введенную регистрационную информацию. Он имеет (среди прочих) методы setName(String)
, setPassword(String)
, bool authenticateUser()
и Authenticator* getAuthenticator()
.
У вас также есть объект класса Authenticator
, который имеет (среди прочих) методы bool isRegistered(String user)
, bool authenticate(String user, String password)
и bool isAuthenticated(String user)
.
Здесь вы можете проверить некоторые простые сценарии:
Создать MockLoginForm
mock со всеми путями, за исключением четырех указанных выше. Первые три метода будут использовать фактическую реализацию LoginForm
; getAuthenticator()
будет пропущен, чтобы вернуться MockAuthenticator
.
Создайте MockAuthenticator
mock, который будет использовать некоторую фальшивую базу данных (например, внутреннюю структуру данных или файл) для реализации трех своих методов. База данных будет содержать только один кортеж: ('rightuser','rightpassword')
.
TestUserNotRegistered
Сценарий воспроизведения:
MockLoginForm.setName('wronuser');
MockLoginForm.setPassword('foo');
MockLoginForm.authenticate();
ожидания:
getAuthenticator() is called
MockAuthenticator.isRegistered('wrognuser') is called and returns 'false'
TestWrongPassword
Сценарий воспроизведения:
MockLoginForm.setName('rightuser');
MockLoginForm.setPassword('foo');
MockLoginForm.authenticate();
ожидания:
getAuthenticator() is called
MockAuthenticator.isRegistered('rightuser') is called and returns 'true'
MockAuthenticator.authenticate('rightuser','foo') is called and returns 'false'
TestLoginOk
Сценарий воспроизведения:
MockLoginForm.setName('rightuser');
MockLoginForm.setPassword('rightpassword');
MockLoginForm.authenticate();
result = MockAuthenticator.isAuthenticated('rightuser')
ожидания:
getAuthenticator() is called
MockAuthenticator.isRegistered('rightuser') is called and returns 'true'
MockAuthenticator.authenticate('rightuser','rightpassword') is called and returns 'true'
result is 'true'
Надеюсь, это поможет.
Ответ 4
Вы можете сделать mock web-сервис довольно эффективно с подклассом NSURLProtocol:
Заголовок:
@interface MyMockWebServiceURLProtocol : NSURLProtocol
@end
Реализация:
@implementation MyMockWebServiceURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
return [[[request URL] scheme] isEqualToString:@"mymock"];
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [[a URL] isEqual:[b URL]];
}
- (void)startLoading
{
NSURLRequest *request = [self request];
id <NSURLProtocolClient> client = [self client];
NSURL *url = request.URL;
NSString *host = url.host;
NSString *path = url.path;
NSString *mockResultPath = nil;
/* set mockResultPath here … */
NSString *fileURL = [[NSBundle mainBundle] URLForResource:mockResultPath withExtension:nil];
[client URLProtocol:self
wasRedirectedToRequest:[NSURLRequest requestWithURL:fileURL]
redirectResponse:[[NSURLResponse alloc] initWithURL:url
MIMEType:@"application/json"
expectedContentLength:0
textEncodingName:nil]];
[client URLProtocolDidFinishLoading:self];
}
- (void)stopLoading
{
}
@end
Интересной процедурой является -startLoading, в которой вы должны обработать запрос и найти статический файл, соответствующий ответу в комплекте приложения, перед перенаправлением клиента на этот файл.
Вы устанавливаете протокол с помощью
[NSURLProtocol registerClass:[MyMockWebServiceURLProtocol class]];
И ссылайтесь на него с такими URL-адресами, как
mymock://mockhost/mockpath?mockquery
Это значительно проще, чем реализация реального веб-сервиса либо на удаленной машине, либо локально внутри приложения; компромисс заключается в том, что симуляция заголовков HTTP-ответов намного сложнее.
Ответ 5
OHTTPStubs - это отличная рамка для того, чтобы делать то, что вы хотите, получившее много тяги. Из их github readme:
OHTTPStubs - это библиотека, предназначенная для быстрого отключения сетевых запросов. Это может помочь вам:
- Проверяйте свои приложения с поддельными сетевыми данными (заглушенными из файла) и имитируйте медленные сети, чтобы проверить поведение вашего приложения в плохих условиях сети.
- Записывайте тесты устройств, которые используют поддельные сетевые данные из ваших светильников.
Он работает с NSURLConnection
, новыми iOS7/OSX.9 NSURLSession
, AFNetworking
(и 1.x и 2.x), либо любой сетевой инфраструктурой, использующей систему загрузки URL Cocoa.
Заголовки OHHTTPStubs полностью документируются с использованием комментариев, похожих на заголовок в форме заголовка. Здесь вы также можете прочитать онлайн-документацию.
Вот пример:
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
return [request.URL.host isEqualToString:@"mywebservice.com"];
} withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
// Stub it with our "wsresponse.json" stub file
NSString* fixture = OHPathForFileInBundle(@"wsresponse.json",nil);
return [OHHTTPStubsResponse responseWithFileAtPath:fixture
statusCode:200 headers:@{@"Content-Type":@"text/json"}];
}];
Вы можете найти дополнительные примеры использования на странице wiki.