Реализация мониторинга и интеграционного тестирования информационной системы с использованием Scalatest. Часть 2

от автора

В первой части статьи было рассмотрено создание проекта в Idea и написание простых тестов. В этой статье будут рассмотрены некоторые особенности работы фреймворка, а также приемы для решения задач, возникающих в ходе написания тестов.
Будут рассмотрены особенности запуска тестов, формирования отчетов, работа с таймаутами, ожиданиями, вызов команд операционной системы.

Особенности запуска тестов
Для запуска отдельного класса запускаем его из Idea, или выполняем команду testOnly в консоли с указанием классов.

sbt "testOnly org.example.MyTest1 org.example.MyTest2" 

Возможно использование символа *, к примеру

sbt "testOnly *MyTest*" 

Запустит все тесты, содержащие в названии «MyTest»

По умолчанию, все классы запускаются параллельно. Можем использовать опцию в build.sbt

parallelExecution in Test := false 

Которая сделает выполнение последовательным.
Более подробно www.scala-sbt.org/0.13/docs/Testing.html

Создание файлов отчетов:
Для того, чтобы после прохождения тестов были сформированы отчеты, необходимо добавить аргументы запуска тестов. Аргументы можно подсмотреть на странице scalatest.org/user_guide/using_the_runner. Не все из них работают для запуска из sbt, нужно экспериментировать.
Дополняем в build.sbt параметр для библиотеки scalatest %«test->*»

libraryDependencies += "org.scalatest" % "scalatest_2.11" % "3.0.0-M15" % "test->*"   Добавляем строку с параметрами   (testOptionsinTest) += Tests.Argument(TestFrameworks.ScalaTest, "-hD", "report", "-fW", "report.txt") 

При этом, после запуска теста, у нас на выходе будет html ("-h") отчет с указанием времени выполнения(«D») в папке «report» и файл текстового отчета(«f») без указания цвета(«W», ANSI Color codes в файле корректно отображаются в Linux системах, в Windows нужны костыли)
Для того, чтобы отчет формировался в кодировке UTF8, не было проблем с русскими символами в Windows, рекомендуется добавить опцию в sbt\conf\sbtconfig.txt

-Dfile.encoding=UTF8 

В папке с отчетом будет файл index.html, а также несколько файлов .html – по одному для каждого класса. В теории, можно использовать русские символы в именах классов, но иногда возникают проблемы с открытием файлов <РусскоеНазваниеТестовогоКласса>.html
Немного изменим класс GetTest, добавив в него методы для сохранения скриншота и вставки в отчет

import org.openqa.selenium.WebDriver import org.openqa.selenium.firefox.FirefoxDriver import org.scalatest.selenium.WebBrowser import org.scalatest.{Matchers, FreeSpec} import scala.io.Source   class GetTest extends FreeSpec with Matchers with WebBrowser{     val pageURL = "http://scalatest.org/about"   def get(url: String) = Source.fromURL(url, "UTF-8").mkString   implicit val webDriver: WebDriver = new FirefoxDriver()   //Указываем директорию для сохранения снимков экрана   setCaptureDir("report")       "Get запрос страницы " + pageURL + " и проверка заголовка" in     {       get(pageURL) should include("<title>ScalaTest</title>")     }     "Открытие страницы %s и проверка заголовка".format(pageURL) in     {       go to pageURL       pageTitle should be ("ScalaTest")       //Создаем снимок экрана       capture to ("MyScreenShot.png")       //Производим вставку в отчет       markup("<a href=\"http://scalatest.org/about\">О скалатест</a>")       markup("<img src='MyScreenShot.png' /> ")     }     "Закрытие браузера" in     {       quit()     } } 

Выполняем в консоли

sbt "testOnly GetTest" 

После прохождения теста, будет сформирован файл «report.txt» в корне проекта и папка «report» c файлами для HTML отчета.
Следует уточнить, что результат выполнения шагов сценария не будет выводиться в консоль — только итоги.

22.01.2016 16:36 — вырезка экрана

В этом и дальнейших примерах с Selenium закрывать браузер будем отдельным шагом

  "Закрытие браузера" in {     quit()   } 

Необходимо, чтобы шаг выполнился последним. Если писать команду закрытия в теле класса, то она выполнится до начала шагов и возникнет ошибка. Чуть позже рассмотрим, как сделать выполнение команд после выполнения всех шагов.

Ожидания:
Существуют ситуации, когда тест падает потому, что отсутствует объект проверки… К примеру, запись в БД появляется через несколько секунд после какого-либо события или элемент на веб странице появляется не сразу, а после отработки сткрипта на клиенте.
В этом случае можно применять паузу (к примеру, команда Thread.sleep(1000) приостановит выполнение кода на 1 секунду), но лучше использовать eventually.
Eventually — позволяет осуществлять периодическое выполнение блока операций до тех пор, пока прохождение не будет успешным, либо не истечет время.
Рассмотрим пример:

class OpenScalatest extends FreeSpec with WebBrowser{   implicit val webDriver: WebDriver = new FirefoxDriver()   val pageURL = "https://www.google.ru/"     "Поиск страницы ScalaTest" in {     go to pageURL     textField("q").value = "ScalaTest"     clickOn("btnG")   }     "Открытие страницы 'ScalaTest'" in {     click on partialLinkText("ScalaTest")   }     "Закрытие браузера" in {     quit()   }   } 

Пример производит поиск в Google страницы ScalaTest и переходит на нее.
Тест упадет с ошибкой «WebElement ‘ScalaTest’ not found.»

Проблема в том, что драйвер считает страницу загруженной после того, как загрузились все ресурсы. Но не учитывает то, что элемент может формироваться на клиенте после загрузки страницы.
Обернем шаг нажатия на ссылку в блок Eventually. Для этого подмешаем трейт в определение класса
«with Eventually»
Импортируем компоненты для работы со временем

Import org.scalatest.time.SpanSugar._ 

Переопределим параметры Eventually

implicit override val patienceConfig = PatienceConfig(timeout = (2 seconds), interval = (250 millis))

И обернем шаг в ожидание  <source lang="scala"> eventually{clickonpartialLinkText("ScalaTest")} 

В итоге получим

class OpenScalatest extends FreeSpec with WebBrowser with Eventually{   implicit val webDriver: WebDriver = new FirefoxDriver()   val pageURL = "https://www.google.ru/"   implicit override val patienceConfig = PatienceConfig(timeout = (2 seconds), interval = (250 millis))     "Поиск страницы ScalaTest" in {     go to pageURL     textField("q").value = "ScalaTest"     clickOn("btnG")   }     "Открытие страницы 'ScalaTest'" in {     eventually{click on partialLinkText("ScalaTest")}   }     "Закрытие браузера" in {     quit()   } } 

Теперь каждые 250 миллисекунд проверяется наличие элемента, если за 2 секунды он не появится — тест упадет.
Более подробно: www.artima.com/docs-scalatest-3.0.0-M15/#org.scalatest.concurrent.Eventually

Существуют случаи, когда блок кода может выполняться очень долго, и нам нужно ограничить время выполнения. Используем трейт Timeouts, который содержит команды failAfter и cancelAfter.
К примеру, если страница грузится более 5 секунд — считаем, что возникла ошибка.

class DevianArt extends FreeSpec with WebBrowser with Timeouts {   implicit val webDriver: WebDriver = new FirefoxDriver()   val pageURL = "http://www.deviantart.com/"     "Открытие страницы %s c ограничением по времени".format(pageURL) in {     failAfter(5 seconds){       go to (pageURL)}   }     "Закрытие браузера" in {     quit()   }   } 

Подробнее на странице
www.artima.com/docs-scalatest-3.0.0-M15/#org.scalatest.concurrent.Timeouts

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

В некоторых случаях существует необходимость выполнения некоторых действий до выполнения шагов теста, после выполнения шагов теста, до или после каждого шага.
Для этого используем трейты BeforeAndAfterAll и BeforeAndAfter
Рассмотрим пример, в котором после каждого шага теста выполняется скриншот, а после всех шагов закрывается браузер

class CreateScalatestCaptures extends FreeSpec with WebBrowser with BeforeAndAfter with BeforeAndAfterAll{   implicit val webDriver: WebDriver = new FirefoxDriver()   val pageURL = "http://www.scalatest.org/"   setCaptureDir("report")     // Метод создает скрин с именем TimeStamp и вставляет его в отчет   def createScreenCaptureToReport(fileName: String = System.currentTimeMillis + ".png"): Unit = {     captureTo(fileName)     markup("<img src='" + fileName + "' width='50%' /> ")   }     "Открытие страницы %s".format(pageURL) in {     go to pageURL   }     "Открытие страницы 'Quick Start'" in {     click on partialLinkText("Quick Start")   }     //Код выполняется после каждого шага теста   after{     createScreenCaptureToReport()   }   //Код выполняется после всех шагов   override def afterAll(){     quit()   }  } 

Небольшое дополнение про Selenium

Скалатест включает в себя SeleniumDSL, примеры применения которого были рассмотрены выше. На странице
scalatest.org/user_guide/using_selenium
Содержится довольно хорошее описание, но есть несколько моментов, которые можно дополнительно озвучить.

В некоторых случаях необходимо управлять размером окна браузера. Это делается с помощью интерфейса
webDriver.manage().window()

К примеру, webDriver.manage().window().maximize() сделает окно развернутым на весь экран (по умолчанию браузер рапускается в оконном режиме),

val browserDimension = new org.openqa.selenium.Dimension(1920,2160) webDriver.manage().window().setSize(browserDimension) 

Сформирует окно размером 1920 *2160

Для переключения между окнами используем код

    val arr = windowHandles.toArray     switch to window(arr(1)) 

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

Выполнение команд ОС:
Иногда может возникнуть необходимость выполнения некоторых команд ОС, на которой запускается тест.
Следующий пример показывает, как проверить количество свободного места на диске и как проверить доступность хоста командой ping.
Для этого создан отдельный объект CMDUtils, в котором происходит определение ОС и выполнение команд

import scala.math._ import scala.sys.process._   object CMDUtils extends Exception{     val os = System.getProperty("os.name").substring(0,3)     def freeSpace: Long = {     var cmd: Seq[String] = null     //Проверка платформы, выполнение команды в зависимости от выбора     os match {       case "Win" => cmd = Seq("powershell", "-command ", "(fsutil volume diskfree c:).split(' ')[-1]")       case "Lin" => cmd = Seq("/bin/sh", "-c", "df / -B1 | sed -n 2p | awk '{print $4}'")       case _  => throw new Exception("ОС не определена")     }     //Выполнение команды и возвращения строки из стандартного вывода     val output = cmd.!!     //Перевод строки с количеством байт в гигабайты и возвращение значения метода     output.trim.toLong/pow(1024,3).toLong   }     def pingHost(host: String): Boolean = {     var cmd: Seq[String] = null     os match {       case "Win" => cmd = Seq("powershell", "-command ", "ping %s | Out-Null ; echo $?".format(host))       case "Lin" => cmd = Seq("/bin/sh", "-c", "ping %s -c 4 &> /dev/null; if (($?==0)); then echo true; else echo false; fi".format(host))       case _  => throw new Exception("ОС не определена")     }     //Выполнение каманды и перевод в true/false     cmd.!!.trim.toBoolean   } }  

Далее методы объекта вызываются из теста

class CMDExecute extends FreeSpec with Matchers{     val limit: Long = 10 // Размер в гигабайтах     "На жестком диске более %s гигабайта свободного пространства".format(limit) in {     CMDUtils.freeSpace should be > limit   }     var host1 = "8.8.8.194"   "Проверка доступности хоста %s".format(host1) in {     CMDUtils.pingHost(host1) should be(true)    }     var host2 = "8.8.8.8"   "Проверка доступности хоста %s".format(host2) in {     //Так тоже можно проверять значение "true"     assert(CMDUtils.pingHost(host2))   } } 

Также с помощью команд ОС возможна отправка результатов выполнения теста на сервер zabbix утилитой

zabbix_sender

Для выполнения сложных операций (запрос к БД, отправка/получение сообщений из менеджера очередей, разбор или формирование XML) пишется отдельный объект, или класс, методы которого вызываются в тесте.
Создание исполняемого jar файла
В некоторых случаях имеет смысл запускать тесты не из проекта, а как отдельный jar файл со всеми зависимостями. При каждом запуске команды «sbt test» происходит компиляция файлов проекта. В случае если изменений не было, то всеравно затрачивается какое –то время на проверку этого. Сборка jar позволит не затрачивать время на компиляцию/сборку каждый запуск теста, а выполнить формирование jar и запускать тест каждый раз без компиляции. Входящие в файл зависимости позволят запускать тест на любой машине, где присутствует java
По умолчанию, в jar файл не входят тесты, поэтому необходимо обеспечить запуск тестовых классов из main кода.
Для этого изменим scope для библиотеки,

libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0-M15" //% "test->*" 

Если отсутствует явное указание, то это «compile» конфигурация.
Подробнее на странице maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
Еще необходимо добавить зависимость, без которой не удастся создать html отчет

 libraryDependencies += "org.pegdown" % "pegdown" % "1.6.0" 

Далее необходимо переместить файлы с тестовыми классами из папки srс/test/scala в папку src/main/scala
После этого создаем объект, назовем его MainApp, который будет запускать наши тесты.

import org.scalatest.tools.Runner   object MainApp extends App{   Runner.run(Array("-s", "TestClass", "-h", "report")) } 

Класс запускает метод run объекта Runner, которому передаются параметры
-s — имя тестового класса для запуска
-h — папка для отчета html

Другие опции можно подсмотреть www.scalatest.org/user_guide/using_the_runner

После этого запускаем sbt run, тест из класса должен пройти и сформировать папку с отчетом и файл с отчетом.
После того, как тесты запускаются командой run, можно собрать jar файл с зависимостями.
Для этого добавим плагин github.com/sbt/sbt-assembly

В файле project/plugins.sbt добавляем строку

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1") 

После обновления проекта при выполнении команды

sbt assembly 

в директори target/scala-2.11 сформируется jar файл
Запустив его командой java -jar .jar мы получим выполнение теста и формирование отчета.

Возможно формировать файл в корне проекта, указав в build.sbt

assemblyOutputPath in assembly := baseDirectory.value / "tests.jar" 

Применение такого подхода приемлемо тогда, когда сборка запускается часто, а меняется редко, либо когда нужно быстро запустить тест на другой машине, куда зависимости для сборки будут загружаться значительное время или отсутствует интернет. Из минусов можно отметить большой размер файла. (40 mb для теста с использованием firefox driver, к примеру) и некоторое время сборки jar файла.

Если необходимо, чтобы при не прохождении тестов был особый exit code (к примеру, чтобы падала сборка на сервере CI), то нужно возвращать код, проверяя значение, возвращаемое ранером

object MainApp extends App{   val res = Runner.run(Array("-s", "TestClass", "-h", "report"))   if (!res) sys.exit(1) }   

Таким образом, были рассмотрены базовые приемы работы с библиотекой, которые позволяют начать писать тесты.
За рамками остались fixtures, mock objects, property-based testing, table-driven testing, использование Sikuli, java Robot для автоматизации тестирования UI и много-много других вкусных плюшек.
Одним из ключевых плюсов фреймворка — наличие отличной документации, с примерами, полным описанием, что делает изучение фреймворка приятным и эффективным.

ссылка на оригинал статьи https://habrahabr.ru/post/281354/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *