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

  • Tutorial


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

Особенности запуска тестов

Для запуска отдельного класса запускаем его из 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" % "2.2.6" % "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 отчета.



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



В этом и дальнейших примерах с 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))

И обернем шаг в ожидание
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 секунды он не появится — тест упадет.
Более подробно: doc.scalatest.org/2.2.6/index.html#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()
  } 
}

Подробнее на странице
doc.scalatest.org/2.2.6/index.html#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))

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

Также существует возможность отслеживания ошибок в консоли браузера.
Нужно сделать импорт
import org.openqa.selenium.logging.{LogType, LoggingPreferences}
import org.openqa.selenium.remote.{CapabilityType, DesiredCapabilities}

В том месте, где объявляем браузер, добавляем строки
var logPref: LoggingPreferences = new LoggingPreferences()
logPref.enable(LogType.BROWSER, Level.SEVERE)//выводим ошибки уровня "SEVERE"
val capability = DesiredCapabilities.firefox()
capability.setCapability(CapabilityType.LOGGING_PREFS, logPref)  
implicit val webDriver: WebDriver = new FirefoxDriver(capability)//создаем экземпляр браузера с возможностью вывода логов

В коде шага пишем для вывода логов в отчет
val browserLogs = webDriver.manage().logs().get(LogType.BROWSER).getAll.toArray
browserLogs.foreach(x=> markup("<q>%s</q>".format(x)))

Если хотим, чтобы шаг фейлился, когда в логах есть ошибки — добавляем
browserLogs.size should be (0)

Выполнение команд ОС

Иногда может возникнуть необходимость выполнения некоторых команд ОС, на которой запускается тест.
Следующий пример показывает, как проверить количество свободного места на диске и как проверить доступность хоста командой 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 для библиотеки, убрав указание «test->*»
libraryDependencies += "org.scalatest" %% "scalatest_2.11" % "2.2.6"

Если отсутствует явное указание, то это «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

Другие опции можно подсмотреть http://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)
} 


Также, тестах есть возможность повторного использования кода. К примеру, создание экземпляра браузера, звкрытие после прохождения теста, авторизация на сайте могут быть выделены в отдельный трейт, от которого наследуются тестовые классы.
Для конфигурирования можно использовать компонент config от typesafe (https://github.com/typesafehub/config)

Таким образом, были рассмотрены базовые приемы работы с библиотекой, которые позволяют писать удобные, универсальные автоматизированные тесты.
За рамками остались fixtures, mock objects, property-based testing, table-driven testing, использование Sikuli, java Robot для автоматизации тестирования UI и много-много других вкусных плюшек.
Одним из ключевых плюсов фреймворка является наличие качественной документации с примерами. Это делает изучение и использование фреймворка приятным и эффективным.
Преимущества данного стека технологий подтверждаются успешным опытом использования в нашей компании.
ООО «ЦИТ» 45,95
ИКТ-решения и сервисы для органов власти и бизнеса
Поделиться публикацией
Комментарии 5
  • +1
    устал скролить, может поправите статью?
    • 0
      Спасибо за замечание. Исправили.
    • 0
      Извините за оффтоп. Слышал что скала комьюнити похоронило язык. Вы не в курсе новостей или же эта новость — бред?
      • +2
        Спасибо за вопрос. Нам самим даже стало интересно.
        Возможно, такой слух связан с ребрендингом одной из самых влиятельных компаний в мире scala — TypeSafe.
        Судя по всему, беспокоиться не о чем. Компания все так же фокусируется на Scala.
        Источник 1
        Источник 2
        Язык активно развивается, скоро выйдет новая версия 2.12, альфа версия нового компилятора и еще много всего.
        Предлагаем ознакомиться с планами по развитию языка
        Scala2016.pdf
        Как видно, активно ведется работа по развитию языка.
        Еще ссылка на интересную статью про сравнение скалы и других языков на основе анализа Reddit.
        how-scala-compares-20-programming-languages-reddit-analysis
        • 0
          Не язык, а компилятор. Не похоронило, а переписало. Не комьюнити, а авторы языка во главе с Одерски. И этого еще не произошло — в процессе. В октябре был бутстрап.

          edit: ответ выше, видимо, более корректен. Я неправильно опознал источник слухов.

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

        Самое читаемое