Back to Home

Testing Ext.Js project on Selenium

Selenium · unit testing · oreodor · extjs

Testing Ext.Js project on Selenium

    There are three things that can be done endlessly:
    1. Watch how the fire burns
    2. Watch how the water runs
    3. And watch how someone works

    In our case, watch how our over 9000 tests spin. Selenium tests look especially beautiful. It looks like a rabid jerboa with a perpetual motion machine inside sat down to test the system.

    I don’t know about you, but it pulls me in:


    The rest of the article I will tell you a little success-story about how we organized our testing on Selenium.


    This was our second approach to the projectile. For the first time, back in 2009, everything rained down:
    • The web driver only worked with IE.
    • Sometimes it paused when I opened the browser
    • Sometimes I couldn’t close the browser
    • And most importantly: the built-in test recording via the browser extension (the so-called “sticking”) did poor tests for the Ext.Js shell.

    Our project is written using Ext.Net - Ext.Js shell for .NET. And Ext.Js was obviously too complicated for Selenium. He generated random identifiers for elements for the client, and additionally random identifiers of objects were also generated by us ourselves (which is necessary for parallelism and high loads of our platform).

    It seems that the test can be easily clicked with the mouse, but the lifetime of this test was a matter of days. Worse, when the test fell, it was often more difficult to understand the cause of the fall than simply reapplying it again (which was done a couple of times).

    And the tests were slow! Imagine a scenario:
    1. We drive a search string into a table with data
    2. The server returns a response - an empty grid (which can sometimes be correct)
    3. And after that sends an error message if it happened.

    Ext.Js with its asynchronous requests for any click is designed so that for many calls we need to wait for an error message of 10 seconds and if we did not receive it, then we thought that the search worked fine.
    10 seconds here, 10 there and as a result, the test itself for 90% of the time consists of waiting.

    And that's not all. Have you tried to figure out the reasons for the fall of the pasted test from Selenium log? So, so as not to run the test again, but to look at the log and understand the essence of the problem. It didn’t work for me.

    After a couple of months, it became clear that such testing takes more time than it does us good and we killed the Selenium tests.

    The project, meanwhile, was growing, the number of functions was measured in the thousands, and now, a year ago, the heroic selmaril returned to this problem with an updated Selenium and an updated understanding of how to test Ext.Js.

    His solution was to write an API.

    We decided to abandon the recording of tests with the mouse, but to come to the opportunity to record NUnit (!) Tests with a short notation in the form:
    1. Filter users by creation date = today.
    2. Sort by name
    3. Open first entry
    4. Change password.
    5. Save.
    6. Click OK if the "Are you sure" window pops up.

    To do this, you needed an API over Selenium and NUnit, which itself would consist of very simple elements, but allowing you to operate not with the mouse and DOM model, but with Ext.Js interface objects.

    A month of hard work, and the first version of the API appeared.
    Here is a sample test:
            /// 
            /// DocumentOperationGridOperationTest
            /// 
            [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("SomeNewField");
                    Assert.False(string.IsNullOrEmpty(fieldValue.GetValue()), "Не заполнилось поле которое должно быть заполнено в классе операции, то есть не вызвался класс операций для документа");
                    grid.DeleteFirstRow();
                    docForm.Close();
                }
            }
    



    And here is what the API implementation looks like
            /// 
            /// Получить элемент из контекстного меню
            /// 
            /// Системное имя поля сущности, в ячейке которой у данной записи надо вызвать контекстное меню
            /// Путь к элементу контекстного меню, по которому надо получить элемент (кнопку действия например)
            /// Найденный элемент в контекстном меню
            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();
                    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;
            }
    



    An important problem was also solved - a quick understanding of the error from the TeamCity log. Api generates such a report for each test.

    Bottom line: now we have covered about 80% of the behavior of the system interface. I’m sharing our experience, for testing Ext.Js on Selenium, you need to write your own wrapper for testing to solve the following problems:
    1. Laconicism and understandability of the test
    2. Resistance of the test to system changes
    3. Speed ​​of the tests
    4. It is easy to understand the reasons for the breakdown of the test
    Tests created by sticking from the browser interface will most likely not suit you.

    PS
    If you have an Ext.Js project and you decide to cover it with Selenium tests - ask for advice, we will try to help.

    Read Next