Распараллеливание тестов или одна голова — хорошо, а две головы — лучше

    В какой-то момент, если долго и усердно стараться сохранять покрытие тестами не меньше 80% кода, прогон полного комплекта тестов начнет занимать больше времени, чем уходит на перекур и на прочтение новых статей хабра. В свою очередь это приводит к тому, что полный комплект (suite) будет запускаться все реже и реже. Hudson начнет сообщать о сломанных билдах, а дальше сработает эффект разбитого окна и сломанный билд станет нормой.

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

    В одном из наших проектов, в который согласно записям redmine вложено около 400 часов работы нашего коллектива ситуация с тестами до распараллеливания выглядела так (пару дней назад):
    151 scenarios (151 passed)
    3997 steps (3997 passed)
    17m49.257s


    18 минут!!!

    За это время разработчик может сварить кофе, выкурить сигарету, сходит в туалет, ущипнуть за попу симпатичную коллегу и успеть посмотреть последние 3 минуты «матрицы» на экране. Если требовать от него чтобы полный прогон запускался перед каждым коммитом, то он только и будет делать что смотреть «матрицу» и щипать попы пить кофе.

    Но анализ загрузки процессора при прогоне показывает, что в работе участвует только лишь одно ядро независимо от того, сколько их всего есть. Как говорит пословица, лучше день потерять, а потом за пять минут долететь. Порыскав в гугле мы нашли гем parallel_tests. Теперь мы не с такой завистью будем смотреть на erlang группу, которые могут спокойно распараллелить свои тесты на кластер арендованных облачных машин в Selectel.

    Гем parallel_tests по сравнению с аналогами (hydra, testjour) выделяется тем, что дает возможность распараллелить именно любимые нами интеграционные тесты на cucumber+capybara+selenium-webdriver, которые из-за обильной аяксификации нашего приложения нельзя выполнить без реального браузера, иными словами — запущенного через гем capybara firefox. При этом реально запускаются несколько копий firefox, на которые более-менее равномерно распределяется нагрузка. htop при этом демонстрирует полную загрузку всех ядер, сколько бы их не было на машине. И, что играет немалую роль, адаптировать приложение для использования этого гема очень просто.

    Во-первых, надо обеспечить существование нескольких тестовых баз, по одной на ядро в типовом случае. Гем сам устанавливает при запуске переменную окружения TEST_ENV_NUMBER, поэтому в database.yml можно прописать в разделе test
    test:
    database: xxx_test<%= ENV['TEST_ENV_NUMBER'] %>

    Естественно, базы надо перед прогоном создать руками или через
    rake parallel:create


    Во-вторых, надо обеспечить синхронизацию схем баз данных при проведении дополнительных миграций или перепроведении старых:
    rake parallel:prepare


    В-третьих, запускать прогон надо по-другому
    rake parallel:features

    Это — для прогона фич cucumber. Можно и rspec и обычные рельсовые тесты гонять.

    Да, гем, конечно же, надо прописать в Gemfile и поставить
    bundle install


    Как результат — вот результаты последней сборки хадсоном на восьмиядерном селектеловском ксеончеге
    Results:
    17 scenarios (17 passed)
    347 steps (347 passed)
    31 scenarios (31 passed)
    780 steps (780 passed)
    8 scenarios (8 passed)
    149 steps (149 passed)
    26 scenarios (26 passed)
    1363 steps (1363 passed)
    26 scenarios (26 passed)
    463 steps (463 passed)
    17 scenarios (17 passed)
    331 steps (331 passed)
    6 scenarios (6 passed)
    88 steps (88 passed)
    19 scenarios (19 passed)
    410 steps (410 passed)

    Took 338.989782947 seconds
    Finished: SUCCESS


    Да, мы уменьшили количество шагов и сценариев, потому что кроме использования параллелизации, код тестов претерпел некий рефакторинг. Но 150 против 151 — не сильно меньше. Зато по времени — 5 с половиной минут против почти 18. Это еще с учетом примерно 60-70 секунд константных затрат на свежий pull из репозитория, перемиграцию баз и запуск файрфоксов.

    Результат налицо, а как следствие, всего за сутки билд в хадсоне застабилизировался — 5 последних коммитов успешно отработали по полному комплекту тестов, чего и всем желаю.

    UPD: Если в параллельном режиме не обнаруживаются step_definitions решение нашел хабрачеловек Alder. Тогда надо запускать тесты так:
    parallel:features[2,'','--require features']
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 38

      +3
      … а две — это мутант.

      А вообще, такие статьи радуют. А то зарываюсь порой чрезмерно.
        +5
        Вы напишите где-нибудь что речь про RoR, а то так и не поймешь сразу.

        Надо будет для python/django что-нибудь такое наковырять. А то на текущем проекте тесты значительно дольше 15 минут отрабатывают…
          +1
          в тэгах будет вполне достаточно
            0
            Спасибо, добавил в тэги
            0
            И статью потом напишите если найдете для django, у нас тесты тоже порядка 20 минут идут, и разбитое окно уже почти наступило
              0
              Нашли для питона подобное?
              +3
              Вот за что рельсы нравятся — когда кажется что жизнь и так малина, все равно найдется гем, который сделает ее еще немножко приятней. С такими гемами BDD перестает напрягать :-)
                –10
                Я один ничего не понял о чем шла речь вообще? :'(
                  +1
                  Да.
                    +1
                    Подумаешь, у меня просто немного рябило все и связи в мозге как-то плохо работали :( щас вроде и не понятно, что мне было не ясно в тот момент))) >_
                  0
                  70 cекунд pull — это может потому что из github.com долго исходники качаются? Может иметь репо где-то поближе?
                    0
                    Нет, гит у нас свой, конечно. 70 секунд — это pull, пересоздание баз (8-ми по числу ядер) и запуск 8 экземпляров firefox.
                      0
                      Мощно. Возможно тогда SSD на тестовой машине / машине с репо сократит это все до десятков секунд.
                        0
                        Машина — в облаке на селектеле в Питере. Похоже, это пока неизбежное зло. Возможно, ramdisk спасет отца русской революции. Попробуем — отпишу.
                    +1
                    Простите, а зачем прогонять интеграционные тесты при каждом коммите? Вполне достаточно только юнит тестов, их 100500 за минуту прогнать можно. А интеграцию или на ночь оставить, или, если время позволяет, в CI, но отдельной конфигурацией.
                      +1
                      То ли у нас проект такой, то ли что, но основная работа происходит именно над видами и контроллерами в терминах MVC. Бизнес логика по сути не сложная и о том как мы покрыли ее cucumberовскими юнит-тестами и они у нас за 20 секунд пробегают — неинтересно. А интеграционные тесты при каждом коммите запускать надо чтобы отловить регрессию в контроллере или сломанный, к примеру, JavaScript на клиентской стороне.
                        0
                        Сломанный JavaScript — это понятно. А какую регрессию можно сунуть в контроллер так, чтобы ее юнит-тесты не поймали? Просто так, интересно.
                          0
                          Ну, например, при рефакторинге гневно снести запись чего-нибудь в сессию, забыв о том, что это используется в другом действии и там тоже надо переделать. Юнит-тесты навряд ли заметят. А интеграционные обнаружат багу, возникшую из-за из-за антипаттерна. Конечно, так делать нельзя, но делают же, потому что есть ситуации, когда не обойтись без сессий.

                          Или можно добавить в модель поле, которое в виде используется, в тестах заполняется, а вот в контроллере редактирования не поправлено и в результате сбрасывается после редактирования. Чтобы это обнаружить надо вызвать несколько действий подряд в определенной последовательности.

                        0
                        И таки да, у нас все это в CI отдельной конфигурацией.
                        0
                        У меня возникла проблема с обнаружением steps definitions. Проделал все по вашим шагам и при выполнении задачи bundle exec rake parallel:features и получаю в результате — все сценарии неопределены. Сталкивались с таким или все сразу заработало?
                          0
                          А без параллелизации работает? Если просто сделать bundle exec rake cucumber?
                            0
                            Да, bundle exec rake сразу ссылается на :cucumber и отлично работает
                              0
                              Попробуйте обновиться. Вот как у нас — для ориентира.
                              Using rake (0.9.2)
                              Using ZenTest (4.5.0)
                              Using RubyInline (3.9.0)
                              Using multi_json (1.0.3)
                              Using activesupport (3.1.0.rc4)
                              Using bcrypt-ruby (2.1.4)
                              Using builder (3.0.0)
                              Using i18n (0.6.0)
                              Using activemodel (3.1.0.rc4)
                              Using erubis (2.7.0)
                              Using rack (1.3.0)
                              Using rack-cache (1.0.2)
                              Using rack-mount (0.8.1)
                              Using rack-test (0.6.0)
                              Using hike (1.1.0)
                              Using tilt (1.3.2)
                              Using sprockets (2.0.0.beta.10)
                              Using tzinfo (0.3.29)
                              Using actionpack (3.1.0.rc4)
                              Using mime-types (1.16)
                              Using polyglot (0.3.1)
                              Using treetop (1.4.9)
                              Using mail (2.3.0)
                              Using actionmailer (3.1.0.rc4)
                              Using arel (2.1.3)
                              Using activerecord (3.1.0.rc4)
                              Using activeresource (3.1.0.rc4)
                              Using addressable (2.2.6)
                              Using barby (0.4.3)
                              Using chunky_png (0.8.0)
                              Using barby-chunky_png (0.3.3)
                              Using bundler (1.0.12)
                              Using nokogiri (1.5.0)
                              Using ffi (1.0.9)
                              Using childprocess (0.1.9)
                              Using json_pure (1.5.3)
                              Using rubyzip (0.9.4)
                              Using selenium-webdriver (0.2.0)
                              Using xpath (0.1.4)
                              Using capybara (1.0.0)
                              Using configuration (1.3.1)
                              Using diff-lcs (1.1.2)
                              Using json (1.5.3)
                              Using gherkin (2.4.5)
                              Using term-ansicolor (1.0.5)
                              Using cucumber (1.0.1)
                              Using cucumber-rails (1.0.2)
                              Using daemons (1.1.4)
                              Using database_cleaner (0.6.7)
                              Using hiredis (0.3.2)
                              Using em-hiredis (0.1.0)
                              Using escape_utils (0.2.3)
                              Using eventmachine (0.12.10)
                              Using em-http-request (0.3.0)
                              Using thin (1.2.11)
                              Using faye (0.6.3)
                              Using haml (3.1.2)
                              Using hoe (2.10.0)
                              Using rack-ssl (1.3.2)
                              Using rdoc (3.8)
                              Using thor (0.14.6)
                              Using railties (3.1.0.rc4)
                              Using rails (3.1.0.rc4)
                              Using kaminari (0.12.4)
                              Using launchy (0.4.0)
                              Using mysql2 (0.3.6)
                              Using parallel (0.5.5)
                              Using parallel_tests (0.5.0)
                              Using png (1.1.0)
                              Using rspec-core (2.6.4)
                              Using rspec-expectations (2.6.0)
                              Using rspec-mocks (2.6.0)
                              Using rspec (2.6.0)
                              Using ruby-ole (1.2.11.1)
                              Using russian (0.2.7)
                              Using sass (3.1.4)
                              Using selenium-client (1.2.18)
                              Using spreadsheet (0.6.5.5)
                              Using sqlite3 (1.3.3)
                                0
                                А имена файлов step_definitions подходят под /^.+_steps\.rb$/?
                                  0
                                  По версиям вроде все ок, разве что рельсы используем 3.0.9 — не думаю, что это критично. Имена файлов подходят — step_definitions/auth_steps.rb к примеру и т.д.
                                    +1
                                    Решение нагуглилось:
                                    parallel:features[2,'','--require features']
                                    и все работает. Можно добавить в статью, если у кого будут подобные проблемы.
                                      0
                                      Спасибо.
                              +1
                              Очередная замечательная статья на тему тестирования в рельсах, спасибо. На новых i7 ускорение должно быть более заметно учитывая количество ядер+ht. Подскажите, на скольки физических/виртуальных ядрах замечен такой рост производительности (чуть более чем в 3 раза, получается)?
                              +2
                              Capybara-webkit попробуйте.
                                0
                                Интересная тема! Быстрее файрфокса?
                                  0
                                  Пишут, что да. По ощущениям тоже. Замеров не делал.
                                    0
                                    У него есть один недостаток — на Гитхабе висит багрепорт от 22 июня о том, что Capybara-webkit не умеет работать с сабдоменами. В нашем приложении это очень важно, поэтому пока используем Firefox.
                                      0
                                      А нам — неважно, так что заценим.
                                        0
                                        Недавно настраивал capybara-webkit, для работы с сабдоменами пришлось написать небольшой модуль с макросами, а так все прекрасно работает. Если решитесь попробовать, могу скинуть исходники и пояснить как настроить.
                                          0
                                          Сейчас уже этой проблемы нет :) Все работает именно в связке capybara+capybara-webkit+cucumber. Но в любом случае будет интересно посмотреть на ваше решение. Можно оформить в виде сниппета на gist.github.com/

                              Only users with full accounts can post comments. Log in, please.