logo

15 авг. 2012 г.

BIEE 11g: регрессионное тестирование с помощью Selenium и JUnit

Привет читателям этого блога!
Сейчас плотно занимаюсь темой тестирования BI проектов.
Как результат – появились интересные наработки, которыми хочу с вами поделиться.

Сегодня я покажу на примере как можно в автоматическом режиме тестировать BI отчетность, а именно – проверять все страницы всех информационных панелей на предмет наличия в них сообщений об ошибках.

Согласитесь, довольно полезный тест. Особенно если инфопанелей много, а предметных областей в репозитории BI мало.
По своему опыту могу сказать: при внесении изменений в RPD никогда точно не знаешь все ли отчеты остались в рабочем состоянии.
И приходится после каждой серьезной правки "прощелкивать" все информационные панели – все ли работает!

Это серьезная трата вашего времени!

(Я не рассматриваю вариантов, когда вам просто плевать работают отчеты или нет: "пользователи проверят")

Действия по проверке отчетов инфопанелей понятны, рутинны, и поэтому могут и должны быть автоматизированы!

Для создания теста мы воспользуемся замечательным (бесплатным) продуктом – Selenium.
В интернете очень много (в том числе на русском языке) инструкций по работе с Selenium IDE, Selenium RC.

Я не буду подробно останавливаться на том, как устроен Selenium, как создавать тесты – тема обширна. И достаточно хорошо освещена по ссылке выше.

Тут же я дам конкретные практические рекомендации как запустить определенный тест.

1. Для начала нам потребуется скачать jar-архивы с классами Selenium RC:
На странице http://seleniumhq.org/download/ в разделе "Selenium RC Server" доступна ссылка http://selenium.googlecode.com/files/selenium-server-standalone-2.25.0.jar

Также в разделе "Selenium Client Drivers" доступна ссылка на скачивание клиентской библиотеки для Java-проектов http://selenium.googlecode.com/files/selenium-java-2.25.0.zip

Сохраним эти файлы, например, в C:/Selenium

2. В папке C:/Selenium создадим bat-файл start-selenium-server.bat со следующим содержимым:
java -jar selenium-server-standalone-2.25.0.jar


Запустим bat-файл.
Тем самым мы получим работающий сервер Selenium'а, который будет "прощелкивать" в браузере страницы ваших информационных панелей.

3. Теперь пора создать сам тест – мы будем писать его на языке Java.
Запустите Eclipse (Java IDE).
Если у вас до сих пор его нет – скачайте http://www.eclipse.org/downloads/ (либо, если вы пользуетесь другой средой, создавайте проект по аналогии с описываемым далее).

3.1) Создайте новый Java-проекта с именем SeleniumTest

3.2) В нем определите package, например, у меня это – ru.servplus.tests

3.3) внутри него создайте новый JUnit Test Case



Укажите тип JUnit 4, задайте имя классу теста – CheckAllDashboardPages, и щелкните чекбоксы для создания методов setUp() и tearDown().



3.4) содержимое созданного класса возьмите отсюда:
package ru.servplus.tests;

import java.io.FileInputStream;
import java.util.Properties;
import java.util.ArrayList;
import java.util.Iterator;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.SeleneseTestBase;
import com.thoughtworks.selenium.Selenium;

public class CheckAllDashboardPages extends SeleneseTestBase {

    private Selenium selenium; 
    private Properties prop;
    
    private String serverHost;
    private int serverPort;
    private String browserType;
    private String browserUrl;
    private String nqUser;
    private String nqPassword;    
    
 @Before
 public void setUp() throws Exception {
        
  prop = new Properties();
  prop.load(new FileInputStream("selenium.properties"));
   
  serverHost = prop.getProperty("serverHost");
  serverPort = Integer.valueOf(prop.getProperty("serverPort"));
  browserType = prop.getProperty("browserType");
  browserUrl = prop.getProperty("browserUrl"); 
  nqUser = prop.getProperty("nqUser");
  nqPassword = prop.getProperty("nqPassword");  
  
        selenium = new DefaultSelenium(
          serverHost,
          serverPort,
          browserType,
          browserUrl);
                
        selenium.start();      
 }

 @After
 public void tearDown() {
  selenium.stop();
 }


 public String getDashboardPath (int i) {    
  StringBuilder script = new StringBuilder();
  script.append("var div1   = window.document.getElementById('idCatalogItemsAccordion');");
  script.append("var div2   = div1.getElementsByTagName('DIV')[0];");
  script.append("var div3   = div2.getElementsByTagName('DIV')[1];");
  script.append("var tab1   = div3.getElementsByTagName('TABLE')[0];");
  script.append("var tbd1   = tab1.getElementsByTagName('TBODY')[0];");
  script.append("var tr1    = tbd1.childNodes[" + i + "];");
  script.append("var td1    = tr1.getElementsByTagName('TD')[0];");
  script.append("var div4   = td1.getElementsByTagName('DIV')[0];");
  script.append("var trows  = div4.getElementsByTagName('TR');");
  script.append("var tr2    = trows[trows.length-3];");
  script.append("var td2    = tr2.getElementsByTagName('TD')[0];");
  script.append("var a1     = td2.getElementsByTagName('A')[0];");

  script.append("a1.listItem.itemInfo.path.toString();");        
  String path = selenium.getEval(script.toString());
  return path;
} 

 @Test
 public void test() {
  //входим в BI под админ. учетной записью (имеет смысл создать отдельную учетку-reader)
     //также крайне важно задать язык сессии, так как по англ. заголовкам элементов будет вестись поиск в тесте 
  selenium.open("/analytics/saw.dll?catalog&nqUser=" + nqUser + "&nqPassword=" + nqPassword + "&lang=en");
  
  //перейдем в режим "Расширенного поиска": будем искать все инфопанели в shared-папках
  selenium.open("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"action\":\"search\",\"location\":\"/shared\",\"mask\":\"*\",\"type\":\"dashboard\",\"favorite\":null}");
    
  int dashboardCount = 0;
  //крайне важно дождаться появления указанных XPath-конструкцией элементов (они догружаются ajax'ом после того как загрузилась сама страница)
  selenium.waitForCondition("selenium.getXpathCount(\"//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr\") > 0", "30000");
  
  //считываем кол-во строк, возвращенных страницей поиска инфопанелей
  dashboardCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr").intValue();
  
  //System.out.println("Dashboard count: "+dashboardCount);
  
  //определяем массив инфопанелей (путь + название) и массив страниц каждой инфопанели
  String[] dashboardPaths = new String[dashboardCount];
  String[] dashboardPathsEncoded = new String[dashboardCount];
  String[][] dashboardPages = new String[dashboardCount][];
  
  //заполняем значениями эти массивы (считываем путь и название каждой инфопанели с помощью XPath-конструкций)
     for(int i=0; i<dashboardCount; i++){
        /*dashboardPaths[i] = selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr[2]/td/a") +
       "/" + selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr[1]/td[2]/span[1]");
        dashboardPaths[i] = dashboardPaths[i].replace("/Shared Folders/", "/shared/").replace("/Dashboards/", "/_portal/");
        */

 dashboardPaths[i] = getDashboardPath(i);
 dashboardPathsEncoded[i] = dashboardPaths[i].replace("\\", "\\\\");
 dashboardPathsEncoded[i] = dashboardPathsEncoded[i].replace("\"", "\\\"");

        //System.out.println(dashboardPathsEncoded[i]);                    
      }
      
      //далее будем считывать значения названий страниц каждой инфопанели
      int dashboardPageCount = 0;
      for(int i=0; i<dashboardCount; i++){
        //не получилось реализовать считывание страниц внутри одного и того же окна браузера
        //возможно, из-за хитрого параметра "type"
        //сейчас реализовано таким образом, что список страниц каждой инфопанели открывается в новом окне
        
        //открываем в новом окне страницу со списком содержимого инфопанели
        selenium.openWindow("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"location\":\"" + dashboardPathsEncoded[i] + "\"}", dashboardPaths[i]);
        selenium.waitForPopUp(dashboardPaths[i], "30000");
        //перемещаем фокус в новое окно
        selenium.selectWindow(dashboardPaths[i]);
        
        //на всякий случай ставим задержку - даем странице успеть отрисоваться
        try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
   
        //убеждаемся, что на странице появились данные о страницах инфопанели
        selenium.waitForCondition("selenium.getXpathCount(\"//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr\") > 0", "10000");

        //считываем кол-во страниц текущей инфопанели
        dashboardPageCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr").intValue();
        
        ArrayList<String> dashboardPageNames = new ArrayList<String>();
        //в цикле считываем наименования страниц инфопанели с помощью Xpath-конструкции
        String dashboardPageName = null;
        for(int j=0; j<dashboardPageCount; j++){
          dashboardPageName = selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (j+1) + "]/td/div/table/tbody/tr[1]/td[2]/span[1]");          
          if (!dashboardPageName.equalsIgnoreCase("_selections") && !dashboardPageName.equalsIgnoreCase("dashboard layout")) {
                    dashboardPageNames.add(dashboardPageName);            
          }
        }

        //закрываем вспомогательное окно              
        selenium.close();        
        
        //перемещаем фокус в главное окно браузера
        selenium.selectWindow("null");          

     dashboardPages[i] = (String[])dashboardPageNames.toArray(new String[dashboardPageNames.size()]);        
      }
      
      int errorCount = 0;
      int allErrorCount = 0;
      String errorText = null;
      String dashboardPageEncoded = null;
      
      //в цикле по всем инфопанелям
      for(int i=0; i<dashboardCount; i++){
        //в цикле по всем страницам текущей инфопанели
        for(int j=0; j<dashboardPages[i].length; j++){

   dashboardPageEncoded = dashboardPages[i][j].replace("\\", "\\\\").replace("\"", "\\\"");       
       
          //открываем страницу инфопанели текущей итерации
          selenium.open("/analytics/saw.dll?Dashboard&PortalPath="+dashboardPathsEncoded[i]+"&page="+dashboardPageEncoded);
          
          //даем странице время отрисоваться
          try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
          //убеждаемся что все "часики" отработали, на всякий случай помещаем в try-блок - если за 30 сек. отчет не отработает
          try {
            selenium.waitForCondition("selenium.getXpathCount(\"//div[contains(text(),'Searching... To cancel, click')]\") == 0", "30000");
        } catch (Exception e) {
          e.printStackTrace();
        }          
          
          //считываем кол-во ошибок (Xpath-проверка по наличию элементов с текстом 'Error Details')
          errorCount = selenium.getXpathCount("//a[contains(text(),'Error Details')]").intValue();          
          allErrorCount = allErrorCount + errorCount;
          
          //Выводим информацию о проблемной странице инфопанели в System.out
          if (errorCount > 0) {
            System.out.println("DASHBOARD: " + dashboardPaths[i]);
            System.out.println("DASHBOARD PAGE: " + dashboardPages[i][j]);
            System.out.println("ERRORS COUNT: " + errorCount);
            
            for(int k=0; k<errorCount; k++){
              errorText = selenium.getText("xpath=(//a[contains(text(),'Error Details')]/parent::div/div/div[2]/div)[" + (k+1) + "]");              
              System.out.println("error["+k+"]: " + errorText);
            }  
            
            System.out.println("========================================================================================================");
          }
        }
      }
      
      //делаем logout текущей BI-сессии
      selenium.click("//span[@id='logout']/span/span");
      
      //закрываем главное окно браузера              
      selenium.close();
      
      //если в ходе теста были обнаружены проблемные страница инфопанелей - тест считаем проваленным
      assertTrue("See System.out for error explanation", (allErrorCount == 0) );      
      
  }
}


3.5) далее следует прописать пути до некоторых библиотек



И указать, что вы хотите включить в проект jar-файлы, полученные на 1-м шаге.

3.6) В тексте класса встречается обращение к файлу selenium.properties – в нем я храню все константы, настраиваемые параметры.

Поэтому создадим файл selenium.properties в корне проекта.



Содержимое файла:
serverHost=localhost
serverPort=4444
browserType=*firefox
browserUrl=http://localhost:7001
nqUser=weblogic
nqPassword=weblogic_1
Исправьте значения параметров browserUrl (хост и порт вашего BI сервера), nqUser, nqPassword под вашу среду.

3.7) На этом можно было бы и остановиться.
Уже теперь, запустив ваш класс теста, вы получите автоматизированную проверку ваших информационных панелей.

Суть теста в эмуляции (благодаря Selenium RC Server, который по умолчанию "слушает" на localhost:4444 входящие запросы) работы браузера.
Нашим java-кодом мы описываем команды firefox-браузеру (browserType-параметр проперти-файла).
Эти команды включают в себя:
- открытие страницы расширенного поиска BIEE
- поиск всех информационныех панелей и сохранение путей до них
- считывание всех страниц каждой инфопанели
- последовательное открытие каждой страницы инфопанелей и поиск сообщений об ошибках.

По окончанию теста, в System.out будут записаны страницы инфопанелей, которые содержат ошибки, и сами тексты ошибок.

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

4. Лично меня напрягает для каждого теста стартовать Eclipse.
Потому пошел чуть дальше – для запуска тестов стал использовать среду "непрерывной интеграции" Jenkins (опять же бесплатную).
И опять прошу меня простить, но тема эта очень обширна, и углубляться в описание этого продукта не буду. В интернете информации очень много.

4.1) Скачаем jenkins.war по ссылке.
Запустим его командой
java -jar jenkins.war

Далее вы можете установить его как сервис в разделе "Manage Jenkins"

4.2) Теперь нам потребуется Apache Ant. Если у вас его нет – скачиваем с сайта http://ant.apache.org/bindownload.cgi
Например, http://www.sai.msu.su/apache//ant/binaries/apache-ant-1.8.4-bin.zip
Распаковываем архив, например, в "c:\Program Files\Ant\apache-ant-1.8.4"

В разделе Manage Jenkins выбираем Configure System и прописываем путь до директории с Ant


4.3) Ant нам был нужен для запуска теста и получения его результатов извне, т.е. из Jenkins. Но чтобы этого добиться – следует в нашем исходном java-проекте в корне создать еще один файл – build.xml.
Он будет содержать инструкции для Ant.
В нашем случае:




 
 
  
   
   
 

  
  
   
   
  

  
  
   
   
  

  
  
   
  

  
  
   
    
     
    
    
     
    
           
  
 

То есть задача Ant-скрипта состоит в очистке папки со скомпилированными классами, папки с результатом выполнения теста;
затем компиляция классов и запуск JUnit-теста (наш класс является JUnit4 тестом) с выводом результатов в XML-файл.

4.3) На главной странице Дженкинс выбираем пункт New Job
И создаем "a free-style software project".
Дадим ему имя - REGR_CheckAllDashboardPages

В домашней директории Jenkins/jobs должна появиться папка REGR_CheckAllDashboardPages/workspace.
Копируем в нее содержимое нашего проекта:



Теперь следует сконфигурировать проект в Jenkins таким образом, чтобы он знал какой экземпляр Ant использовать (у меня это Ant184, настроенный на уровне всей системы) и какую цель (target) следует Ant запускать (у нас это test) в ходе сборки-build'а (все-таки это система непрерывной интеграции и тут все крутится вокруг билдов).

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



И последний штрих – следует скопировать в lib-директорию Ant'а все новые jar-файлы Selenium'а и jar-файл JUnit4



4.4) И теперь вы можете как разово запускать новые сборки билдов проекта, так и "зашедулить" их.
В результате у вас будет храниться история сборок билдов, где каждый будет содержать отчет о выполнении Selenium-теста.





P.S. Наверное, для больших, распределенных команд разработки под OBIEE будет полезно применять Jenkins полностью – то есть задействовать все шаги непрерывной интеграции:
- По коммиту в SVN/GIT запускать сборку билда.
- Получать из SVN последние изменения.
- Применять эти изменения на некотором тестовом сервере BI (с помощью вызова shell/bat-файлов для копирования RPD-файлов, веб-каталога; с помощью WLST для остановки и запуска BI компонентов и т.д.).
- Запускать «прогоны» всех регрессионных тестов (согласитесь, наш сегодняшний тест - регресионный).
- По результату выполнения тестовы отсылать письма-уведомления всем участникам проекта с перечнем ошибок.
Выглядит интересно…

P.P.S. В следующий раз расскажу как организовать нагрузочное тестирование вашего BI сервера.

13 комментариев:

  1. Благодаря одному из читателей выяснилось, что приведенное решение "спотыкается" в случае:
    - наличия скрытых инфопанелей;
    - наличия описаний для инфопанелей;
    - наличия спецсимволов("/") в названии инфопанели.

    В ближайшие дни внесу исправления.

    ОтветитьУдалить
  2. Отличная идея! На какой версии BI это прогонялось? У меня на 11.1.1.5 летит эксепшен

    com.thoughtworks.selenium.SeleniumException: ERROR: Element //div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[29]/td/div/table/tbody/tr[2]/td/a not found
    at com.thoughtworks.selenium.HttpCommandProcessor.throwAssertionFailureExceptionOrError(HttpCommandProcessor.java:112)
    at com.thoughtworks.selenium.HttpCommandProcessor.doCommand(HttpCommandProcessor.java:106)
    at com.thoughtworks.selenium.HttpCommandProcessor.getString(HttpCommandProcessor.java:275)

    ОтветитьУдалить
  3. Роман, работает на версии 11.1.1.6.2.
    У коллег с 11.1.1.5 тоже вылетает ошибка.
    Но как выяснилось, ошибка из-за наличия описаний для инфопанелей (в момент определения списка инфопанелей). С предновогодней суетой руки не доходят подправить... В начале января обязательно что-нибудь придумаю!

    ОтветитьУдалить
  4. Готов всячески помогать и тестировать! С наступающим НГ!!!

    ОтветитьУдалить
  5. Роман, спасибо за поздравления!!! ;)

    Внес изменения в java-код (как в тексте поста, так и в архиве проекта по ссылке).
    Изменения учитывают наличие спец.символов (двойные кавычки, обратные слеши) в названиях инфопанелей/страниц инфопанелей; наличие описания для инфопанелей/страниц.

    Суть изменений в том, что для определения абсолютного пути инфопанели в структуре веб-каталога используется вызов функции getDashboardPath, которая в свою очередь с помощью getEval обращается к javascript-функции.
    Также добавлена явная обработки символов обратного слеша и двойных кавычек в названиях инфопанелей/страниц.

    ОтветитьУдалить
    Ответы
    1. Спасибо! Попробовал новую версию, не отрабатывает поиск dashboards, т.е. открывается диалог поиска с
      пустыми полями и дальше вылет по таймауту. Если же не дожидаться таймаута, а ввести данные руками и нажать search, то дальше все работает замечательно...

      Удалить
    2. Роман, возможно, вы пытаетесь выполнить тесты под какой-нибудь совсем "кастрированной" учетной записью...
      Попробуйте войти в BI и открыть затем URL:
      http://YOUR_SERVER_HOST:PORT/analytics/saw.dll?catalog&action=searchpanel&type=all#{"action":"search","location":"/shared","mask":"*","type":"dashboard","favorite":null}

      Удалить
    3. Из браузера под тем же логином отрабатывает нормально,но все равно придется "курить" XPath, т.к. у нас каждый dashboard имеет кучу закладок и запрос
      selenium.openWindow("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"location\":\"" + dashboardPathsEncoded[i] + "\"}", dashboardPaths[i]);
      просто открывает их список. Получается, что надо делать еще 1 вложенный цикл.

      Удалить
    4. сорри, непонятно написал.... проще или скриншотов сделать пачку или вебконференцию.

      Удалить
  6. Спасибо большое.

    ОтветитьУдалить