Привет, Хабр! Представляю вашему вниманию перевод статьи "Testing Your RxSwift Code" автора Shai Mishali с сайта raywenderlich.com. Написание реактивных приложений с помощью RxSwift концептуально отличается от написания приложений "обычным путем". Оно отличается в том смысле, что объекты в вашем приложении обычно не будут иметь четкого значения, вместо этого они будут представлены потоком значений на оси времени, известным в RxSwift как Observable. Эта статья даст вам ключ к тестированию RxSwift кода. Observable — мощный механизм, который позволяет вам, как разработчику, реагировать на изменения и быть уверенным что состояние вашего приложения всегда актуально. Вместе со всеми преимуществами которые это дает, тестирование Observable — не такая тривиальная задача, как простое использование XCTAssert для обычных значений. Но не переживайте — эта статья наставит вас на путь становления экспертом в тестировании RxSwift! Эта статья научит вас создавать unit тесты для Observable потоков. Вы научитесь некоторым доступным техникам для тестирования RxSwift кода, а также некоторым советам. Давайте начнем. Реактивные приложения прекрасно показывают себя, когда надо работать с изменяющимся контентом, так что мы будем работать как раз с таким приложением. Загрузите стартовый проект. Вы найдете стартовый проект для этого урока: Raytronome — приложение-метроном, которым вы бы могли воспользоваться для занятий музыкой. Как вы можете понять, из-за того что метрономы работают со временем, вам предстоит увидеть много логики, которую можно протестировать. Откройте Raytronome.xcworkspace. После откройте Main.storyboard. Вы увидите, что это очень простое приложение с всего одним экраном.  Соберите и запустите приложение. Нажмите на кнопку Play чтобы запустить метроном. Вы также можете изменить тактовый размер (Signature) и темп (Tempo).  Приложение состоит из одного UIViewController — MetronomeViewController.swift, а MetronomeViewModel.swift содержит всю бизнес-логику, для которой вы и будете писать тесты. Быстро повторим основы RxSwift и Observable потоков. Работа с потоками кардинально отличается от работы с простыми значениями и объектами, задача тестирования потоков отличается соответственно. Простые значения единственны и независимы; у них нет никого представления во времени. Observable потоки, с другой стороны, выдают элементы (значения) во времени.  Это означает, что при тестировании потоков значений вам часто придется тестировать одну из двух вещей: Обычно хорошей идеей будет немного подумать о том, что именно вы хотите тестировать. Как было упомянуто ранее, вы будете тестировать MetronomeViewModel, ViewModel которая содержит бизнес-логику относящуюся к вашему метроному.  Откройте MetronomeViewModel.swift. Смотря на ViewModel вы можете увидеть выходы, которые отвечают за разные части логики: числитель, знаменатель (тактового размера), тактовый размер, темп в виде строк, числовое значение числителя, максимальное значение числителя, а также потоки отвечающие за бит  Давайте подумаем о том что нам захочется протестировать в UI. Мы хотим протестировать что: В процессе написания тестов вы будете пользоваться двумя дополнительными библиотеками вместе с RxSwift, которые называются RxBlocking и RxTest. Каждая из них предлагает разные возможности и концепты для тестирования ваших потоков. Эти библиотеки уже входят в ваш стартовый проект Стартовый проект включает в себя полупустой тестовый таргет с файлом RaytronomeTests.swift. Откройте его и осмотритесь; он импортирует RxSwift, RxCocoa, RxTest и RxBlocking, а также включает в себя поле viewModel и простую функцию setUp() для создания нового экземпляра MetronomeViewModel перед каждым тестом Ваши первые тесты будут проверять, что числитель и знаменатель начинаются со значения 4. Это означает, что вы будете думать только о первом значении, которое выдаст каждый из потоков. Звучит как идеальная задача для RxBlocking! RxBlocking — это одна из двух библиотек для тестирования доступных с RxSwift, и он следует простой идее: он позволяет вам перевести ваш Observable поток в BlockingObservable, особый Observable который блокирует текущий поток, ожидая некоторого условия в зависимости от оператора.  Он полезен в ситуациях, когда вы либо имеете дело с конечной последовательностью — то есть той, которая отдает событие completed или error — либо хотите протестировать конечное число элементов RxBlocking дает несколько операторов, из которых самые полезные:  Очевидно, что для нашей ситуцаии наиболее подходящим будет оператор first(). Добавьте следующие тесты к классу RaytronomeTests: Вы используете toBlocking() для того чтобы конвертировать свой обычный поток в BlockingObservable, а потом используете first() чтобы подождать и вернуть первый выданный элемент. Далее вы можете использовать XCTAssert как и в любом обычном тесте. Обратите внимание, что методы включают в себя throws, так как операторы RxBlocking могут возвращать ошибки. Добавление аннотации throws помогает избежать конструкции вида try! и корректно обработать падающий тест. Нажмите Command-U для запуска тестов  В качестве упражнения попробуйте написать еще два теста которые проверят что signatureText имеет начальное значение 4/4, а tempoText имеет начальное значение 120 BPM. Эти тесты будут очень похожи на тесты выше Если вам не удается сделать это самостоятельно, можете посмотреть решение под спойлером ниже  Как вы могли заметить, RxBlocking эффективен и прост в освоении, так как он в некотором роде "оборачивает" реактивные концепты в хорошо известные структуры. К сожалению, это накладывает некоторые ограничения о которых вам стоит знать: Следующие тесты которые вам предстоит написать упираются почти во все из этих ограничений. Например: Нажатие кнопки Play/Pause должно вызывать событие в потоке isPlaying, а это требует асинхронного вызова (tappedPlayPause вход). Также следует проверить количество вызовов. Как было упомянуто ранее, RxBlocking очень удобен, но его функционала может не хватать когда доходит до тонкого тестирования событий вашего потока, времени, и связей с другими асинхронными триггерами. Для разрешения этих и других проблем приходит RxTest! RxTest кардинально отличается от RxBlocking, главным образом тем, что он гораздо более гибок в своих возможностях, и той информацией которую он дает о ваших потоках. Это возможно из-за того, что он дает свой планировщик — TestScheduler.  Перед тем как начинать писать код стоит разобраться в том что же такое планировщик Планировщик — немного более низкоуровневый концепт RxSwift, но тем не менее очень важно понимать что это такое, как они работают, для того чтобы лучше понимать их роль в ваших тестах RxSwift использует планировщики как абстракцию для описания выполнения задач, а также для упорядочивания событий появившихся в результате выполнения этих задач.  Вы можете задать вопрос: "Зачем об этом знать?" RxTest дает свой кастомный планировщик — TestScheduler — исключительно для тестирования. Он упрощает тестирование событий зависимых от времени, позволяя вам создать мокнутые Observable и Observer, так что вы сможете "записать" их и протестировать.  Если вы хотите глубже разобраться в планировщиках — официальная документация предлагает много полезной информации на эту тему. Перед тем как писать тесты вам нужно создать экземпляр объекта TestScheduler. Вы также добавите DisposeBag в ваш класс для управления Disposable которые создаются вашими тестами. Ниже поля viewModel добавьте следующие поля: Потом, в конце метода setUp(), добавьте следующие строки для создания новых TestScheduler и DisposeBag перед каждым тестом: Инициализатор TestScheduler принимает аргумент initialClock, который определяет "стартовое время" вашего потока. Новый DisposeBag позаботится о том чтобы избавиться от всех подписок. оставленных прошлым тестом. Давайте писать тесты! Ваш первый тест "нажмет" кнопку Play/Pause несколько раз и убедится, что выход isPlaying реагирует соответствующе. Для этого вам необходимо: Это может казаться сложным, но вы удивитесь как просто это на самом деле! Некоторые вещи проще обьяснить на примере. Начните с добавления первого RxTest-теста: Не переживайте, если это выглядит сложным. Давайте разберем по частям: Запутались? Подумайте об этом таким образом: вы создаете мок потока событий и скармливаете его во вход viewModel в конкретные моменты времени. Потом вы проверяете, что выход отдал ожидаемые события в ожидаемое время.  Прогоните свои тесты еще раз нажатием Command-u. Вы должны увидеть 5 успешных тестов.  Возможно вы заметили, что ранее мы использовали 0, 10, 20 и 30 в качестве времени и задумались о том что эти значения показывают, и как они соотносятся с реальным временем. RxTest использует внутренний механизм для перевода реального времени (Date) в то, что называется VirtualTimeUnit (выраженный через Int). При планировании событий с RxSwift время которые вы используете может быть любым — они совершенно случайны и TestScheduler использует их для планирования событий как любой другой планировщик. Важная вещь, которую стоит держать в голове — это то, что виртуальное время на самом деле не соотносится с реальным, то есть, 10 не означает 10 секунд, а только отражает виртуальное время. Вы узнаете немного больше о внутренностях этого механизма чуть позже в этой статье. Теперь, когда у вас есть более глубокое понимание времени в TestScheduler, почему бы вам не вернуться к добавлению тестов? Добавьте эти три теста сразу после предыдущего: Эти три теста делают следующее: Надеюсь, вы уже чувствуете себя комфортно с этим к кодом, так как он довольно сильно похож на предыдущий тест. Вы отправляете события о том, что числитель изменился на 3, а затем на 1. И вы проверяете, что numeratorText отдает "4" (стартовое значение), "3", и, в итоге, "1". Похожим образом вы проверяете, что изменение значения знаменателя меняет denominatorText соответствующе Наконец, вы проверяете что изменение темпа правильно меняет строковое отображение с концом BPM Запустите тесты нажатием Command-U, вы увидите 8 прошедших тестов. Отлично!  Хорошо, кажется вы все понимаете! Время немного все сложнить. Добавьте следующий тест: Глубоко вздохните! Тут на самом деле нет ничего ужасного или пугающего, это лишь чуть более длинная версия тестов, которые вы уже писали. Вы добавляете элементы сразу во входы steppedNumerator и steppedDenominator по очереди для создания разных видов тактов, а далее вы проверяете что выход signatureText выдает правильно форматированные такты.  Это становится более понятно если посмотреть на тест визуально:  Не стесняйтесь запустить все тесты еще раз. Теперь у вас 9 рабочих тестов! Далее, вы победите еще более сложный кейс. Подумайте о следующем сценарии: Обратите внимание: Несмотря на то, что некоторые из этих тактов имеют место быть в музыке, мы будем считать их неверными для нашего метронома Добавьте тест для такого сценария: Несколько запутанно, но ничего такого, чего вы бы не поняли! Разберем по частям:  Вы написали довольно сложный тест! Запустите тесты нажатием Command-U: О нет! Тест провалился. Смотря на результат кажется, что выход numeratorValue остается равен 24, даже когда знаменатель падает, что оставляет вас с невалидными темпами вроде 24/16 и 24/4. Запустите приложение и попробуйте сами:  Похоже, вы нашли баг. :] Конечно мы сделаем правильный выбор и поправим его. Откройте MetronomeViewModel.swift и найдите этот кусок кода, отвечающий за выставление numeratorValue: Замените его на: Вместо того чтоб просто брать значение steppedNumerator и отдавать его, вы совмещаете последнее значение steppedNumerator и maxNumerator и возвращаете меньшее. Запустите ваши тесты снова нажатием Command-U, и вы должны увидеть 10 прекрасных удачных тестов. Прекрасная работа!  Вы прошли довольно далеко в тестировании viewModel. Если вы посмотрите на покрытие кода тестами, то вы увидите цифру около 78%. Время поднять его до самого верха! Обратите внимание: Для того чтобы увидеть покрытие кода, нажмите Edit Scheme... в дроп-дауне со схемами, и, в разделе Tests, выберите вкладку Options и отметьте Code Coverage. Выберете Gather coverage for some targets и добавьте таргет Raytronome в этот список. После следующего запуска тестов в Report Navigator появится процент покрытия кода тестами Осталось всего две вещи, которые осталось покрыть. Первая — сам бит генерируемый метрономом. Мы хотим убедиться, что при некотором выбранном такте/скорости, бит вызывается в равные интервалы, и что сам бит правильный (первый удар каждого цикла отличается от остальных) Вы начнете с тестирования самого быстрого знаменателя — 32. Вернитесь к RaytronomeTests.swift и добавьте следующий тест:  Пока что этот тест не будет проходить. Но все же разобьем его на части: Запустите тесты нажатием Command-U. Вы увидите следующий результат сравнения: Судя по всему события выдают корректные значения, но время выглядит странно, не так ли? Просто список из чисел от 1 до 8. Для понимания, что что-то не так, попробуйте изменить счетчик с 4/32 на 4/4. Это должно выдать события в другое время, как и сам бит. Замените Meter(signature: "4/32") на Meter(signature: "4/4") и запустите тесты снова нажатием Command-U. Вы должны увидеть такую-же ошибку с идеально таким-же временем. Воу, это странно! Обратите внимание, что вы получили события в такое-же время, как и в первый раз. Как так вышло, что разные такты выдали события в одно время? Что-ж, это связано с VirtualTimeUnit, который уже был упомянут в этой статье раньше.  Используя стандартный темп в 120 BPM, и используя знаменатель 4 (как для 4/4), вы должны получать один удар бита каждые 0.5 секунд. Используя знаменатель 32, вы должны получать удар каждые 0.0625 секунд. Для понимаения того, почему это — проблема, вам нужно лучше разобраться в том как TestScheduler переводит реальное время, в свои внутренние VirtualTimeUnit. Вы вычисляете виртуальное время разделяя настоящие секунды на нечто называемое resolution и округляя результат наверх. resolution — часть TestScheduler и имеет стандартное значение 1. 0.0625/1 округленное наверх равно 1, но округление 0.5/1 также дает 1, что просто недостаточно точно для теста подобного рода.  К счастью, вы можете изменить resolution, для получения большей точности для теста подобного рода. Выше инициализации viewModel, на первой строке вашего теста, добавьте следующую строку: Это уменьшит resolution и даст более высокую точность при округлении виртуального времени Обратите внимание насколько виртуальное время меняется при уменьшении resoulution  Переключите свой счетчик обратно на 4/32 в инициализаторе viewModel и снова запустите тесты нажатием Command-U. Вы наконец получите более разделенные временные отметки, которые вы сможете проверять. Удары наконец разделены виртуальным временным промежутком в 6. Теперь вы можете заменить существующий XCTAssertEqual с этим: Запустите тесты еще раз нажатием Command-U, и вы должны увидеть, что тест наконец проходит. Отлично! Использование такой-же метод для тестирования бита 4/4 очень похоже. Добавьте следующий тест: Единственное отличие тут — это то, что вы подняли resolution, до 0.1, так как он дает достаточную точность для знаменателя 4. Запустите свои тесты снова нажатием Command-U, и вы должны увидеть, что все 12 тестов теперь проходят! Если вы посмотрите на покрытие кода тестами, то вы увидите, что у вас 99.25% покрытого кода для MetronomeViewModel, что прекрасно. Единственный выход который вы не протестировали: beatType.  Тестирование beatType — неплохое испытание для себя на этом этапе, так как тест должен быть очень похож на предыдущие два теста, за тем исключением, что beatType должен переключаться между .even и .odd. Попробуйте написать этот тест сами. Если вы почувствуете, что застряли, можете посмотреть ответ под спойлером: Вы можете скачать законченную версию проекта, использую кнопку в начале статьи. Вы знаете все, что вам нужно для того чтобы начать тестировать приложения основанные на RxSwift. Вы узнали как полезен RxBlocking для тестирования конечных последовательностей где вы не заинтересованы во времени происходящих событий, тогда как RxTest дает вам огромную гибкость и мощь для тестирования. И вы даже дотронулись до более низкоуровневых объектов, таких как Scheduler, а также узнали как TestScheduler рассчитывает виртуальное время. Есть еще много того, что можно узнать как в RxSwift, так и в RxBlocking — их внутреннюю работу, операторы и так далее. Лучшее место для продолжения — официальная документация RxSwift, а также список операторов RxBlocking. Если у вас остались вопросы, или комментарии по содержанию статьи — добро пожаловать в комментарии, или в обсуждении к оригинальной статье. Спасибо за прочтение!Внимание, данная статья является переводом статьи с сайта raywenderlich.com. iOS разработчик