Работа с AngularJS Protractor из C # и Java

    Введение


    Как лозунг на Angular.org гордостью объясняет:
    Angular is what HTML would have been, had it been designed for applications , что в вольном переводе звучит так: Angular является тем, чем был бы HTML — если бы он с самого начала был предназначен для создания (веб -) приложений. AngularJS был разработан с нуля, чтобы быть тестируемым. Но многие разработчиков Selenium хотят продолжать использовать свои существующие Java или C # кодовую базу и навыки но обнаруживают при переключении на тестирование AngularJS SPA и MVVM веб-приложений, что Protractor, лидирующий инструмент тестирования приложегий AngularJS, написан на JavaScript тоже.

    К счастью, Protractor довольно легко портируется на другие языки — он использует небольшое подмножество протокола JsonWire на котором основан Selenium WebDriver, а именно всего один интерфейс.


    За короткое время был дополнен и развит проект protractor-net представляющий порт существующих методов Protractor https://github.com/angular/protractor/blob/master/lib/clientsidescripts.js из Javascript на C# и затем другой проект, выполняющий ту же задачу из Java.
    Для тестирования был выбран сайт http://www.way2automation.com на котором среди прочего есть и проект для AngularJS,
    http://www.way2automation.com/angularjs-protractor/banking.

    тесты представляют собой «стандарные» действия «клиента» и «менеджера» банка «XYZ Bank» по проверке баланса, созданию учетных записей, проведения платежей и т.п. — это позволило проиллюстрировать все имеющиеся методы. Вызов тестов осуществлен из проекта на C# и из Java

    Примеры кода


    C#


    «Клиент» заходит, выбирает счет, заносит сумму, и когда транзакция прошла, проверяет баланс (есть и тест на съем средств — тут он не показан, смотрите архив).


    image

    
      [TestFixture]
      public class Way2AutomationTests
      {
          private StringBuilder verificationErrors = new StringBuilder();
          private IWebDriver driver;
          private NgWebDriver ngDriver;
          private WebDriverWait wait;
          private IAlert alert;
          private string alert_text;
          private Regex theReg;
          private MatchCollection theMatches;
          private Match theMatch;
          private Capture theCapture;
          private int wait_seconds = 3;
          private int highlight_timeout = 100;
          private Actions actions;
          private String base_url = "http://www.way2automation.com/angularjs-protractor/banking";
    
          [TestFixtureSetUp]
          public void SetUp()
          {
              driver = new FirefoxDriver();
              driver.Manage().Timeouts().SetScriptTimeout(TimeSpan.FromSeconds(60));
              ngDriver = new NgWebDriver(driver);
              wait = new WebDriverWait(driver, TimeSpan.FromSeconds(wait_seconds));
              actions = new Actions(driver);
          }
    
          [SetUp]
          public void NavigateToBankingExamplePage()
          {
              driver.Navigate().GoToUrl(base_url);
              ngDriver.Url = driver.Url;
          }
    
          [TestFixtureTearDown]
          public void TearDown()
          {
              try
              {
                  driver.Close();
                  driver.Quit();
              }
              catch (Exception) { } 
              Assert.IsEmpty(verificationErrors.ToString());
          }
    
          [Test]
          public void ShouldDeposit()
          {
              ngDriver.FindElement(NgBy.ButtonText("Customer Login")).Click();
              ReadOnlyCollection<NgWebElement> ng_customers = ngDriver.FindElement(NgBy.Model("custId")).FindElements(NgBy.Repeater("cust in Customers"));
              // select customer to log in
              ng_customers.First(cust => Regex.IsMatch(cust.Text, "Harry Potter")).Click();
    
              ngDriver.FindElement(NgBy.ButtonText("Login")).Click();
              ngDriver.FindElement(NgBy.Options("account for account in Accounts")).Click();
    
    
              NgWebElement ng_account_number_element = ngDriver.FindElement(NgBy.Binding("accountNo"));
              int account_id  = 0;
              int.TryParse(ng_account_number_element.Text.FindMatch(@"(?<result>\d+)$"), out account_id);
              Assert.AreNotEqual(0, account_id);
    
              int account_amount = -1;
              int.TryParse(ngDriver.FindElement(NgBy.Binding("amount")).Text.FindMatch(@"(?<result>\d+)$"), out account_amount);
              Assert.AreNotEqual(-1, account_amount);
    
              ngDriver.FindElement(NgBy.PartialButtonText("Deposit")).Click();
    
              // core Selenium
              wait.Until(ExpectedConditions.ElementExists(By.CssSelector("form[name='myForm']")));
              NgWebElement ng_form_element = new NgWebElement(ngDriver, driver.FindElement(By.CssSelector("form[name='myForm']")));
    
    
              NgWebElement ng_deposit_amount_element = ng_form_element.FindElement(NgBy.Model("amount"));
              ng_deposit_amount_element.SendKeys("100");
    
              NgWebElement ng_deposit_button_element = ng_form_element.FindElement(NgBy.ButtonText("Deposit"));
              ngDriver.Highlight(ng_deposit_button_element);
              ng_deposit_button_element.Click();
              
              // inspect status message
              var ng_message_element = ngDriver.FindElement(NgBy.Binding("message"));
              StringAssert.Contains("Deposit Successful", ng_message_element.Text);
              ngDriver.Highlight(ng_message_element);
    
              // re-read the amount
              int updated_account_amount = -1;            
              int.TryParse(ngDriver.FindElement(NgBy.Binding("amount")).Text.FindMatch(@"(?<result>\d+)$"), out updated_account_amount);
              Assert.AreEqual(updated_account_amount, account_amount + 100);
          }
    

    Java


    «Клиент» заходит, выбирает счет, смотрит транзакции, умеет найти записи «Credit».


    @Test
        public void testListTransactions() throws Exception {
          // customer login
          ngDriver.findElement(NgBy.buttonText("Customer Login")).click();
          // select customer/account with transactions
          assertThat(ngDriver.findElement(NgBy.input("custId")).getAttribute("id"), equalTo("userSelect"));
    
          Enumeration<WebElement> customers = Collections.enumeration(ngDriver.findElement(NgBy.model("custId")).findElements(NgBy.repeater("cust in Customers")));
          
          while (customers.hasMoreElements()){
            WebElement next_customer = customers.nextElement();
            if (next_customer.getText().indexOf("Hermoine Granger") >= 0 ){
              System.err.println(next_customer.getText());
              next_customer.click();
            }
          }
          NgWebElement login_element = ngDriver.findElement(NgBy.buttonText("Login"));
          assertTrue(login_element.isEnabled());
          login_element.click();
    
          Enumeration<WebElement> accounts = Collections.enumeration(ngDriver.findElements(NgBy.options("account for account in Accounts")));
          
          while (accounts.hasMoreElements()){
            WebElement next_account = accounts.nextElement();
            if (Integer.parseInt(next_account.getText()) == 1001){
              System.err.println(next_account.getText());
              next_account.click();
            }
          }
          // inspect transactions
          NgWebElement ng_transactions_element = ngDriver.findElement(NgBy.partialButtonText("Transactions"));
          assertThat(ng_transactions_element.getText(), equalTo("Transactions"));
          highlight(ng_transactions_element);
          ng_transactions_element.click();
          wait.until(ExpectedConditions.visibilityOf(ngDriver.findElement(NgBy.repeater("tx in transactions")).getWrappedElement()));
          Iterator<WebElement> ng_transaction_type_columns = ngDriver.findElements(NgBy.repeaterColumn("tx in transactions", "tx.type")).iterator();
          while (ng_transaction_type_columns.hasNext() ) {
            WebElement column = (WebElement) ng_transaction_type_columns.next();
            if (column.getText().isEmpty()){
              break;
            }
            if (column.getText().equalsIgnoreCase("Credit") ){
              highlight(column);
            }
          }
        }
    

    Для интерактивного тестирования, стоит запустить Selenium-ноду и хаб локально на порт 4444

    @BeforeClass
    public static void setup() throws IOException {
      DesiredCapabilities capabilities =   new DesiredCapabilities("firefox", "", Platform.ANY);
      FirefoxProfile profile = new ProfilesIni().getProfile("default");
      capabilities.setCapability("firefox_profile", profile);
      seleniumDriver = new RemoteWebDriver(new URL("http://127.0.0.1:4444/wd/hub"), capabilities);
      try{
        seleniumDriver.manage().window().setSize(new Dimension(600, 800));
        seleniumDriver.manage().timeouts()
          .pageLoadTimeout(50, TimeUnit.SECONDS)
          .implicitlyWait(20, TimeUnit.SECONDS)
          .setScriptTimeout(10, TimeUnit.SECONDS);
      }  catch(Exception ex) {
        System.out.println(ex.toString());
      }
      ngDriver = new NgWebDriver(seleniumDriver);
    }
    

    Для билда используем

    @BeforeClass
    public static void setup() throws IOException {
      seleniumDriver = new PhantomJSDriver();
      wait = new WebDriverWait(seleniumDriver, flexible_wait_interval );
      wait.pollingEvery(wait_polling_interval,TimeUnit.MILLISECONDS);
      actions = new Actions(seleniumDriver);
      ngDriver = new NgWebDriver(seleniumDriver);
    }
    

    Особенности


    Синхронизация


    Для динамических страниц в дополнение / вместо многообразных порой весьма трудночитаемых методов проверки того как отдельные элементы страницы выглядят или что с ними происходит, которые предлагает «core Selenium»:
    elementSelectionStateToBe ( By locator, boolean selected)
    проверяет, что данный элемент выбран или нет
    elementToBeClickable ( By locator)
    элемент доступен
    stalenessOf ( WebElement element)
    пока элемент больше не прикреплены к DOM
    textToBePresentInElementLocated ( By locator, java.lang.String text)
    текст присутствует в элементе, который найдет данный локатор
    textToBePresentInElementValue ( By locator, java.lang.String text)
    текст присутствует в присутствует в выбранном атрибуте элемента который найдет данный локатор
    visibilityOfAllElementsLocatedBy ( By locator)
    проверка, что все элементы, которые найдет соответствуют локатор, видны на веб-странице

    (фрагмент взят из документации https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/support/ui/ExpectedConditions.html), Protractor вызывет Angular напрямую

    public boolean isSelected() {
      this.ngDriver.WaitForAngular();
      return this.element.isSelected();
    }
    
    public void WaitForAngular() {
      if (!this.IgnoreSynchronization){
        this.jsExecutor.executeAsyncScript(ClientSideScripts.WaitForAngular, this.rootElement);
      }
    }
    
    

    посылая

    var el = document.querySelector(arguments[0]);
    var callback = arguments[1];
    angular.element(el).injector().get('$browser').notifyWhenNoOutstandingRequests(callback);
    

    и/или

    var rootSelector = arguments[0];
    var callback = arguments[1];
    if (window.getAngularTestability) {
        window.getAngularTestability(el).whenStable(callback);
        return;
    }
    

    этот метод вызывается изо всех стандартных действий с элементами страницы перед тем как будет вызван «core» метод, например:

    public bool Displayed
    {
      get {
        this.ngDriver.WaitForAngular();
        return this.element.Displayed;
      }
    }
    


    и в итоге тестируемый сайт и тестовый сценарий оказываются хорошо синхронизованы без каких-либо дополнительных усилий.

    Создание тестов



    Вместо того чтобы копировать CSS Selector и XPaths нужного элемента, расработчик теста смотрит на шаблон страницыwww.way2automation.com/angularjs-protractor/banking/depositTx.html

      <span class="error" ng-show="message" >{{message}}</span><br>            
    


    и её контроллер
    www.way2automation.com/angularjs-protractor/banking/depositController.js

      if (txObj.success) {
          $scope.message = "Deposit Successful";
      } else {
          $scope.message = "Something went wrong. Please try again.";
      }
    


    чтобы произвести проверку:
    
    // inspect message
    var ng_message = ngDriver.FindElement(NgBy.Binding("message"));
    StringAssert.Contains("Deposit Successful", ng_message.Text);
    ngDriver.Highlight(ng_message);
    
    


    Дополнительные возможности


    Protractor позволяет не только находить, но и вычислять интересующие нас объекты:
    
      [Test]
      public void ShouldEvaluateTransactionDetails()
      {
          ngDriver.FindElement(NgBy.ButtonText("Customer Login")).Click();
          // select customer/account with transactions
          ngDriver.FindElement(NgBy.Model("custId")).FindElements(NgBy.Repeater("cust in Customers")).First(cust => Regex.IsMatch(cust.Text, "Hermoine Granger")).Click();
          ngDriver.FindElement(NgBy.ButtonText("Login")).Click();
          ngDriver.FindElements(NgBy.Options("account for account in Accounts")).First(account => Regex.IsMatch(account.Text, "1001")).Click();
    
          // switch to transactions
          NgWebElement ng_transaction_list_button = ngDriver.FindElement(NgBy.PartialButtonText("Transactions"));
          StringAssert.Contains("Transactions", ng_transaction_list_button.Text);
          ngDriver.Highlight(ng_transaction_list_button);
          ng_transaction_list_button.Click();
    
          // wait for transaction information to be loaded and rendered
          wait.Until(ExpectedConditions.ElementExists(NgBy.Repeater("tx in transactions")));
    
          // examine first few transactions using Evaluate      
          ReadOnlyCollection<NgWebElement> ng_transactions = ngDriver.FindElements(NgBy.Repeater("tx in transactions"));
          int cnt = 0;
          foreach (NgWebElement ng_current_transaction in ng_transactions) {
            if (cnt++ > 5) { break; }
            StringAssert.IsMatch("(?i:credit|debit)", ng_current_transaction.Evaluate("tx.type").ToString());
            StringAssert.IsMatch(@"(?:\d+)", ng_current_transaction.Evaluate("tx.amount").ToString());
            // 'tx.date' is in Javascript UTC format similar to UniversalSorta­bleDateTimePat­tern in C# 
            var transaction_date = ng_current_transaction.Evaluate("tx.date");
            StringAssert.IsMatch(@"(?:\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)", transaction_date.ToString());
          }
      }
    
    


    Полный список тестов на 01.08.2016:

    C#
    • ShouldAddCustomer
    • ShouldDeleteCustomer
    • ShouldDeposit
    • ShouldEvaluateTransactionDetails
    • ShouldListTransactions
    • ShouldLoginCustomer
    • ShouldOpenAccount
    • ShouldSortCustomersAccounts
    • ShouldWithdraw




    Java (десктоп)
    • testAddCustomer
    • testCustomerLogin
    • testDepositAndWithdraw
    • testEvaluateTransactionDetails
    • testListTransactions
    • testOpenAccount
    • testSortCustomerAccounts


    Java (CI/travis)
    • testAddition
    • testChangeSelectedtOption
    • testEvaluate
    • testFindElementByOptions
    • testFindElementByRepeaterColumn
    • testFindElementByRepeaterWithBeginEnd
    • testFindSelectedtOption



    CI тесты — упрощенные, тестируеумые страницы загружаются прямо с диска:

    
    String localFile = "bind_select_option_data_from_array_example.htm";
    URI uri = NgByIntegrationTest.class.getClassLoader().getResource(localFile).toURI();
    ngDriver.navigate().to(uri.toString()));
    

    для выполнения упрощенного сценария напр.
    
          Iterator<WebElement> options = ngDriver.findElements(NgBy.repeater("option in options")).iterator();
          while (options.hasNext() ) {
                    WebElement option = (WebElement)  options.next();
            
            if (option.getText().isEmpty()){
              break;
            }
            if (option.getText().equalsIgnoreCase("two") ){
                        option.click();
                    }
                }
                NgWebElement element = ngDriver.findElement(NgBy.selectedOption("myChoice"));
          assertThat(element.getText(),containsString("two"));    
    


    вызывается драйвер phantomJS

    Статья (более подробная версия) также опубликована мною на Code Project, туда же периодически загружаются наиболее свежие архивы проектов. Оба проекта на гитхабе:

    — полностью рабочие, коммиты практически каждый день.
    Метки:
    • +8
    • 19,1k
    • 6
    Поделиться публикацией
    Похожие публикации
    Комментарии 6
    • –2
      Лучше уж JSDOM для E2E тестирования использовать, а не Selenium суррогаты.

      Фронтендерам проще самостоятельно покрывать свой JS фронтенд тестами на JS'е, и не зависить от разработчиков бэкенда, на чём бы он не был написан. По этому я считаю нецелесообразным портировать тот же protractor под C# или что либо другое.
      • 0
        если вас интепесует мое мнение, то мне кажется что под Practor код получается чище и его легче сопровождать но это не повод для holy war.
        статью(и) написал потому что незаслуженно заброшенная тема (не могу поверить что пользовались проектами protractor-jvm и protractor-net в которых существенная (по — моему ) часть функционала не была внедрена потому что разработчики потеряли к теме интерес
        • 0
          Я не говорю что протрактор плох, или что его не стоит использовать, я просто не вижу смысла использовать его с Java/Net, и есть решения гораздо эффективнее (jsdom). Почти все известные мне фронтендеры пишут приёмочные тесты сами под себя на JS'е. Бэкенд с REST эндпоинтами имеет смысл покрывать тестами на C#/Java в случае реализации гипермедиатипов (hateoas), кодогенерации, или шаблонных контроллеров.

          Можете пожалуйста аргументировать вашу точку зрения: почему кому-то стоит использовать E2E тестирование для JS проекта на С#/Java?
          • 0
            Добавил в статью описание специфических возможностей Protractor. Мне кажется есть много клиетнов которые уже имеют значительные объемы бизнес приложений на Spring и ASP и присматриваются или внедряют Angular (банки, медицина) — они могут быть заинтересованы в сохранении существующей code base
      • 0
        Почему не стали использоваться page object подход?
        • 0
          хорошй вопрос.
          не успел — потому это все сделано совсем недавно — буквально на каникулах дня благодарения и рождествениских каникулах — совершенно один. точнее upstream проекты с которых я начал (форкнув) живут уже давно но по моему заброшены (больше чем полтора года ) не обновлялись. я связался с Carlos Alexandro Becker и он вроде одобрил PR но как то продолжения не вижу.
          C Aaron «F1tZy» Van Prooyen связаться не удалось.
          А Bruno Baia вообще в прощлом году отказался рассматривать PR в котором просили добавить ButtonText (у кого-то другого) потому что это типа не кошерный AngularJS хотя в AngularJS Protractor он прекрасно есть.

          так что милости просим форк и присоединяйтесь к разработке.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.