Реалізація моніторингу та інтеграційного тестування інформаційної системи з використанням Scalatest. Частина 2



У попередній статті Реалізація моніторингу та інтеграційного тестування інформаційної системи з використанням 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 Браузера{ 
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» з файлами HTML звіту.



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



У цьому і подальших прикладах з Selenium закривати браузер будемо окремим кроком
"Закриття браузера" in {
quit()
}

Необхідно, щоб крок виконався останнім. Якщо писати команду закриття в тілі класу, то вона виконається до початку кроків і виникне помилка. Трохи пізніше розглянемо, як зробити виконання команд після виконання всіх кроків.

Очікування
Існують ситуації, коли тест падає тому, що відсутній об'єкт перевірки… наприклад, запис у БД з'являється через кілька секунд після якої-небудь події або елемент на веб сторінці з'являється не відразу, а після відпрацювання сткрипта на клієнта.
В цьому випадку можна застосовувати паузу (наприклад, команда Thread.sleep(1000) призупинить виконання коду на 1 секунду), але краще використовувати eventually.
Eventually — дозволяє здійснювати періодичне виконання блоку операцій до тих пір, поки проходження не буде успішним, або не закінчиться час.
Розглянемо приклад:
class OpenScalatest extends FreeSpec with Браузера{
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 Браузера 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 Браузера 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 Браузера 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 для бібліотеки, прибравши вказівку «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"

Далі необхідно перемістити файли з тестовими класами з папки ѕгс/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 і багато інших смачних булочок.
Одним з ключових плюсів фреймворку є наявність якісної документації з прикладами. Це робить вивчення і використання фреймворку приємним і ефективним.
Переваги даного стека технологій підтверджуються успішним досвідом використання в нашій компанії.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.