Тестирование проекта Ext.Js на Selenium

    Есть три вещи, которые можно делать бесконечно:
    1. Наблюдать, как горит огонь
    2. Наблюдать, как бежит вода
    3. И наблюдать, как кто-то работает

    В нашем случае, наблюдать за тем, как крутятся наши over 9000 тестов. Особенно красиво смотрятся Selenium тесты. Выглядит, как будто бешеный тушканчик с вечным двигателем внутри сел тестировать систему.

    Не знаю как вас, но меня это затягивает:


    Остаток статьи я расскажу маленькую success-story о том, как мы организовали наше тестирование на Selenium


    Это был наш второй подход к снаряду. В первый раз, еще в 2009 году, сыпалось все:
    • Web драйвер работал только с IE.
    • Иногда подвисал при открытии браузера
    • Иногда не мог закрыть браузер
    • И самое главное: встроенная запись тестов через расширение браузера (т.н. «накликивание») делала плохие тесты для Ext.Js оболочки.

    Наш проект написан с использованием Ext.Net – оболочки Ext.Js для .NET. А Ext.Js был явно слишком сложен для Selenium. Он генерировал для клиента случайные идентификаторы элементов, и дополнительно случайные идентификаторы объектов так-же генерировали мы сами (что необходимо для параллелизма и высоких нагрузок нашей платформы).

    Вроде бы тест можно легко накликать мышкой, но время жизни этого теста составляло – считанные дни. Хуже того, когда падал тест, то понять причину падения часто было сложней, чем просто перенакликать его заново (что и делали пару раз).

    А еще тесты были медленные! Представьте сценарий:
    1. Мы вбиваем строку поиска в таблицу с данными
    2. Сервер возвращает ответ – пустой грид (что иногда может быть правильным)
    3. И вдогонку шлет сообщение об ошибке, если она произошла.

    Ext.Js с его асинхронными запросами на любой клик устроен так, что для многих вызовов нам нужно ждать сообщение об ошибке секунд 10 и если мы его не получали, то считали, что поиск отработал нормально.
    10 секунд здесь, 10 там и в итоге сам тест на 90% времени состоит из ожидания.

    И это еще не все. А вы пробовали разобраться в причинах падения накликанного теста из Selenium лога? Вот так, чтобы повторно не запускать тест, а окинуть взглядом лог и понять суть проблемы. У меня – не получалось.

    Через пару месяцев стало понятно, что такое тестирование отнимает у нас больше времени чем приносит пользы и Selenium тесты мы убили.

    Проект тем временем все рос, количество функций измерялось тысячами и вот, год назад героический selmaril вернулся к этой проблеме с обновленным Selenium и обновленным пониманием, как тестировать Ext.Js

    Его решение было: писать API.

    Мы решили отказаться от записи тестов мышкой, но прийти к возможности записывать NUnit(!) тесты с короткой нотацией в виде:
    1. Отфильтровать пользователей по дате создания = сегодня.
    2. Отсортировать по имени
    3. Открыть первую запись
    4. Изменить пароль.
    5. Сохранить.
    6. Нажать ОК, если выскочит окно «Вы уверены».

    Для этого было нужно API над Selenium и NUnit, которое само состояло бы из очень простых элементов, но позволяющих оперировать не мышкой и DOM моделью, а с объектами интерфейса Ext.Js.

    Месяц напряженной работы, и первая версия API появилась.
    Вот образец теста:
            /// <summary>
            /// DocumentOperationGridOperationTest
            /// </summary>
            [Test]
            public void DocumentOperationGridOperationTest()
            {
                var baseDocName = typeof(BaseInDocument).ModelName();
                var implDocName = typeof(BaseInDocumentImpl).ModelName();
    
                using (var grid = Env.Navigation.OpenList(implDocName))
                {
                    var operationCaption = "Документик_Создайся";
                    grid.Toolbar.Click(operationCaption);
    
                    var docForm = Env.TabPanel.GetForm(implDocName);
    
                    docForm.Toolbar.Click("Создание", "Завершить операцию {0}".FormatWith(operationCaption));
    
                    docForm.Close();
                }
    
                using (var grid = Env.Navigation.OpenList(baseDocName))
                {
                    var operationCaption = "Редактируем_Наследник";
                    grid.Data.First().Select();                
                    grid.Toolbar.Click("Операции", operationCaption);
                    grid.Toolbar.Click("Открыть");                
                    var docForm = Env.TabPanel.GetForm(implDocName);
                    var fieldValue = docForm.GetField<TextField>("SomeNewField");
                    Assert.False(string.IsNullOrEmpty(fieldValue.GetValue()), "Не заполнилось поле которое должно быть заполнено в классе операции, то есть не вызвался класс операций для документа");
                    grid.DeleteFirstRow();
                    docForm.Close();
                }
            }
    



    А вот как выглядит реализация API
            /// <summary>
            /// Получить элемент из контекстного меню
            /// </summary>
            /// <param name="fieldName">Системное имя поля сущности, в ячейке которой у данной записи надо вызвать контекстное меню</param>
            /// <param name="buttonCaption">Путь к элементу контекстного меню, по которому надо получить элемент (кнопку действия например)</param>
            /// <returns>Найденный элемент в контекстном меню</returns>
            public IToolbarElement GetContextMenuItem(string fieldName, string[] buttonCaption)
            {
                var buttonsFullPath = Oreodor.Utils.EnumerableExtensions.ToString(buttonCaption, "->");
                Env.AddHistory("Получить элемент в контекстном меню ячейки для записи с Id - SysName = {0} - {1}, для колонки {2}, по пути '{3}'".FormatWith(
                                                                                                                                                             Id,
                                                                                                                                                             this.Data.ContainsKey("SysName") ? this.Data["SysName"] : string.Empty,
                                                                                                                                                             fieldName,
                                                                                                                                                             buttonsFullPath));
    
                IToolbarElement menuItem = null;
    
                if (buttonCaption.Length > 0)
                {
                    var menuItems = new List<IToolbarElement>();
                    if (IsContextMenuVisible(fieldName))
                    {
                        menuItems.AddRange(GetCurrentContextMenu());
                    }
                    else
                    {
                        menuItems.AddRange(ShowContextMenu(fieldName));
                    }
    
                    menuItem = Toolbar.CheckItem(menuItems, buttonCaption[0]);
    
                    if (buttonCaption.Length > 1)
                    {
                        for (var i = 1; i < buttonCaption.Length; i++)
                        {
                            var menuCaption = buttonCaption[i - 1];
                            var itemCaption = buttonCaption[i];
    
                            var menu = menuItem.Menu.ToList();
    
                            Assert.That(menu.Count() != 0, "Меню '{0}' не должно быть пустым, т.к. в нем ещё надо найти '{1}'.".FormatWith(menuCaption, itemCaption));
    
                            var candidateToolbarItem = Toolbar.CheckItem(menu, itemCaption);
    
                            Assert.That(candidateToolbarItem != null, "Проверка наличия элемента '{0}' в меню '{1}'. Найдены следующие элементы: {2}".FormatWith(itemCaption, menuCaption, menu.Aggregate(", ", (aggregated, item) => aggregated + "'" + item.Text + "'")));
    
                            menuItem = candidateToolbarItem;
                        }
                    }
                }
    
                return menuItem;
            }
    



    Также была решена важная проблема – быстрое понимание ошибки из лога TeamCity. Api генерирует вот такой отчет по каждому тесту.

    Итог: сейчас мы покрыли около 80% поведения интерфейса системы. Делюсь выжимкой нашего опыта, для тестирования Ext.Js на Selenium Вам нужно писать свою обертку для тестирования для решения следующих проблем:
    1. Лаконичность и понятность теста
    2. Устойчивость теста к изменением системы
    3. Скорость работы тестов
    4. Легко разобраться в причинах поломки теста
    Тесты, созданные накликиванием из интерфейса браузера скорее всего Вам не подойдут.

    P.S.
    Если у вас Ext.Js проект и вы решите покрыть его Selenium тестами — обращайтесь за советами, постараемся помочь.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      +1
      Ext.Js с его асинхронными запросами на любой клик устроен так, что для многих вызовов нам нужно ждать сообщение об ошибке секунд 10 и если мы его не получали, то считали, что поиск отработал нормально.
      10 секунд здесь, 10 там и в итоге сам тест на 90% времени состоит из ожидания.


      А эта проблема в итоге была решена или нет?
        0
        Да, в каждом конкретном случае в API selmaril нашел детерминированный способ определить, нужно ждать ответа дальше или нет. Как раз была проблема в том, что при внешне похожих условиях нужно было применять различные алгоритмы разрешения. Если сами тестируете, можно его поспрашивать.
          +1
          Да, решил! Правда потребовался сдвиг мышления.

          Все же мы тестируем высокоуровневый функционал, то, как компоненты ведут себя вместе, как они реализуют системный функционл. А ещё тестируем сложные цепочки вызовов, которые не из интерфейса сложно проверить. Ну и гарантируем то, что версия продукта не будет иметь очевидных проблем с точки зрения конечного пользователя (на покрытых участках, естественно).

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

          Так вот, тесты во время выполнения делают инъекции в тестируемый код *увернулся от помидорки*. Правда очень ограниченные, и эти инъекции не должны изменить логику тестируемого стенда.

          В частности для AJAX, перед вызовом добавляется подписчик, который регистрирует этот вызов в глобальной коллекции. А по выполнению (или на отказ) вешается другой подписчик, который во первых изменяет состояние глобальной коллекции, а во вторых зачищает за собой весь мусор.

          Таким образом мы пуляем событие на выполнение AJAX (жмем кнопку, обновляем список и т.п.), и после этого начинаем долбить Selenium-ом браузер на предмет завершенности запроса в данной глобальной коллекции. Выходит что нам не важно что должно будет прийти на AJAX запрос (а там такой адЪ приходит порой), нам важно чтоб в принципе запрос завершился. И нам не надо ждать больше положенного. Соответственно пока Selenium не убедился что запрос закончен, он не передает управление прикладному коду теста. Это упрощает тест. Разработчику теста даже не надо думать что его кнопочка что-то там куда-то услала, он просто проверяет что после того, как нажал кнопочку появилось окошко с надписью «Учапу»! И это прекрасно ^_^ А ещё мы вместо тестера перехватываем все технические ошибки (запрос не прошел, вернулся не тот формат, требуется аутендификация и т.п.).

          Вот как-то так.
          +1
          Удивило, кстати, в ваших демках, что вы приложения не минифицируете. 200+ http-запросов как с куста :-)
            0
            Ага, при первой загрузке довольно много запросов. Тут у нас основная идея в том, чтобы статические ресурсы запрашивались (и кэшировались) отдельными запросами, что позволит отдельно управлять таким кэшем. Соответственно, при повторном запуске запросов должно быть меньше
            www.webpagetest.org/result/130603_0R_PZ1/ (в этом примере 167 и 11)

            Но частично проблема и в том, что все наши текущие клиенты — корпоративные. Приложения на нашем ядре работают во внутренних сетях или на широкополосном доступе, и поводов ТЩАТЕЛЬНО оптимизировать количество запросов у нас меньше, чем могло бы быть :)
              +1
              Но частично проблема и в том, что все наши текущие клиенты — корпоративные. Приложения на нашем ядре работают во внутренних сетях или на широкополосном доступе, и поводов ТЩАТЕЛЬНО оптимизировать количество запросов у нас меньше, чем могло бы быть :)


              у нас в компании тоже есть такое корпоративное приложение — только оно расположено в штатах, а наш офис через VPN туда подключается. Без мата работать не возможно.

              Ааа, есть еще одно «корпоративное» приложение — salesforce называется, тоже не летает.
                0
                Кстати, а действительно, по сравнению с этими двумя приложениями по скорости работы наше скорее тупит или скорее летает? Какое Ваше мнение?
                  +1
                  Ух ты, а вот и первое упоминание Salesforce, встреченное без специального гугления на просторах интернетов :)
                    0
                    Первый твой комментарий, не то что без специального гугления, без каких либо ожиданий встретить его в такой теме!
              +1
              API над Selenium и NUnit, которое само состояло бы из очень простых элементов, но позволяющих оперировать не мышкой и DOM моделью, а с объектами интерфейса Ext.Js

              Есть надежда, что это надстройка появится в виде opensource-проекта?
                +1
                Я бы поучаствовал в идее и каркасе OpenSource ядра тестирования, но не уверен что смогу найти время чтоб поднять такой проект самому. Все же текущее ядро достаточно специализированно, и оно тем лучше, чем лучше отражает бизнесс объекты тестируемой системы. Цель универсальности не ложится в постановку на эту работу увы :\ Но если готовы поучаствовать, то можно замутить что-то :) Я бы объяснил в деталях как оно работает. В общем велкам, в ЛС.
                  0
                  В одиночку, конечно, сложно, но ведь в том и смысл opensource, чтобы найти помощников, которые тоже заинтересованы в появлении такой библиотеки-надстройки. У меня в настоящий момент коммерческих проектов с использованием ext.js не имеется, так что мой интерес не непосредственно практический, а общее развитие экосистемы вокруг Selenium — больше надстроек хороших и разных!
                0
                Я вот не понял немного на счет реализации тестирования API, так сейчас тесты иду через браузер путем выполнения JavaScript кода из C#, или тестируются внутренние .NET компоненты ExtJS или тестируются HTTP запросы?
                  0
                  Все напоминает простой юнит тест, только с селениумом.
                  Сперва вы пишите свой АПИ. Например для формочки логина пишете класс форма_логина, у неё два инпута — тоже классы. У каждого есть свои методы, например у инпутов — ввести текст, у формы — сделать сабмит.

                  На самом нижнем уровне (например сделать сабмит формы) вылезает силениум. Примерно так:
                  SubmitForm(idOfSubmitButton)
                  {
                  webDriver.SelectById( idOfSubmitButton ).Click(); // вызвать кнопку на форме по ID и нажать на неё произведя сабмит
                  }
                  И тест состоит из последовательности таким методов типа SubmitForm()
                  И всё. Запустив тест вы видите как всё само тестируется. Фиксяться тесты также быстро. Самое долгое — написать API для всех UI элементов.
                  Скорость тестирования некоторых тестов в сравнении с людьми сокращается с 2х часов до 30 минут (пример из жизни). Количество манки-тестировщиков можно уменьшить до 1 (проверять вёрстку и т.п.)

                  Фактически это эмуляция поведения человека (перетаскивание мышки, клики, ожидание, проверка текста)
                    0
                    Тест идет через браузер (с помощью специального драйвера Selenium). Собственно, в этом суть Selenium.
                    API (которое посылает команды драйверу на клики в браузер) и сам тест написан на C#.
                      0
                      А как же вы решили проблему с автогенерироваными ID?
                      На этом примере я вижу элементы с айди ext-gen-#### которые генерируются при каждом обновлении страницы.

                      dev.sencha.com/deploy/ext-4.0.0/examples/desktop/desktop.html
                        +2
                        Если в кратце, то наберите в отладчике на указанной вами странице это:

                        Ext.ComponentManager.each(function (id, item){console.log(id, item.getXTypes(), item.initialConfig)})

                        В консоль выведется весь список компонентов. Из них уже можно вытянуть Id DOM узла, или ссылку на него (Ext.getCmp('id').body.dom — например). Далее этот Id или ссылку (да, да, именно ссылку на DOM) можно передать в Selenium.

                        Тут все более менее понятно. Вопрос как найти нужный компонент.

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

                        Получается мы можем восстановить иерархию компонентов с одной стороны, и с другой стороны выцепить нужный компонент. А самому Selenium не нужен именно Id, ему нужна ссылка на DOM узел. Да но можно передавать и Id, дело вкуса.

                        Кстати, ExtJS так устроен, что идентификатор компонента соответствует идентификатору корневого DOM узла из верстки этого компонента. Т.е. можно по Id найти контейнер и по нему кликнуть, а по контейнеру можно найти компонент, и выцепить информацию о его конфигурации, где могут быть определяющие назначение этого компонента зарубки.

                        + Можно в некоторых случаях и DOM шаблон компонента переопределить. Но это трудозатратно и нудно, я так делать перестал, от идеи отказался.
                          +1
                          Спасибо. Это действительно интересный подход.
                    +1
                    Вопрос немного не по теме:
                    1. Если у вас старый проект, то как вы мигрируете между ключевыми версиями extjs или остались на старой версии?
                    2. Перешли ли на сборку проекта средствами Sencha CMD?
                    3. Если используете rest store, то как избавились от генерации уймы запросов при изменении нескольких сотен записей в store за один проход?
                    4. Как реализуете первоначальную загрузку данных с сервера при инициализации приложения — генерируете десяток запросов к бекенду через стандартные store.load(); или реализовали собственный загрузчик?
                    5. Как обстоят дела с синхронизацией записей в store, если одновременно с программой работают десятки людей?
                      +1
                      Вы исходите из предположения, что клиент на ExtJS у нас толстый, но это не так. Он очень тоненький и не является источником, либо хранилищем данных (за исключением редактируемой в данный момент формы). Клиент — это лишь отображение данных.

                      А ещё мы используем Ext.NET, так что необходимости собирать ExtJS у нас нет.

                      Теперь по вопросам:
                      1. Текущий проект был переписан на ExtJS 3 с нуля. На ExtJS 4 происходит миграция, с болью конечно, но за пару месяцев управлюсь. У нас очень много изменений в компонентах.
                      2. Нет необходимости. Ресурсами управляет Ext.NET, он сам собирает скрипты.
                      3. Редактируемые GridPanel у нас есть, но REST store мы не использовали. Самостоятельно обходим набор данных хранилища, формируем в один запрос все изменения и шлем на сервер. Это обусловлено сложной структурой данных, отображаемых в редактируемом списке, и их не полной транзитивностью.
                      4. Такой необходимости нет, клиент оооочень тонкий. Все что он загружает при старте — дерево навигации, да пяток компонентов-виджетов (асинхронно по событиям).
                      5. Одновременно с программой работают десятки людей. Проблемы с синхронизацией не возникают. В принципе сервер управляет этим, отправляя команды обновления наборов данных в некоторых случаях. Конкретная запись данных версионна и это проверяется на сервере. Так что с синхронизацией проблемы решать не пришлось.

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