Блеск и нищета фреймворков тестирования: используем Groovy для скриптинга end-to-end тестов
Меня зовут Андрей, я BigData Java разработчик. В этой статье я расскажу как запускать тесты, написанные на groovy без gradle, maven и даже без предварительной компиляции этих тестов, а также почему JUnit 5 - это намного больше, чем просто хорошая библиотека для unit тестов.
Технологии приходят и уходят. Вне зависимости от внешних факторов, необходимость в разработке остается. Как остается и потребность в тестировании. Не всё можно отдать на откуп unit тестам, нужны end-to-end тесты. Желательно автоматизированные и непрерывные. В свою очередь автоматизация тестирования - это одновременно и сложно, и просто (ведь все разработчики считают, что написание тестов это плевое дело). По этому поводу на хабре недавно вышла замечательная статья Как правильно (не) использовать тестировщиков.
Хорошо, когда тестируется система, которую тестируют многие: например, для Web есть такие монстры как Selenium WebDriver.
Хорошо, когда есть удобное окружение: запустил пару десятков docker образов, и система для теста готова.
Плохо, когда надо тестировать систему, которую не любят тестировать, а удобным окружением и не пахнет.
Предположим, карты сошлись так, что у вас есть:
система, для работы с которой есть только библиотеки на Java;
"неудобное окружение для тестирования", которое предполагает отсутствие подключения к репозиториям артефактов и невозможность использовать maven/gradle для компиляции и сборки, но имеется возможность задеплоить тестовые артефакты по scp и подключиться по ssh.
Вроде всё просто:
разрабатываем тестовый сценарий и реализуем его на любимом фреймворке (JUnit, Spock, TestNG, да хоть класс c psvm);
собираем код для тестов на CI сервере;
развертываем в тестовую среду;
запускаем;
собираем результаты.
Кажется никаких проблем, но начнем с самого первого шага. Предположим, у нас уже есть тестовый сценарий, и мы хотим его слегка поменять, прогнать и посмотреть, что получится. Или вообще пишем первый сценарий и хотим поэкспериментировать. Пару циклов "написать", "собрать", "задеплоить", "запустить" и вот уже пропадает всякое желание писать новый тесткейс. Тут и появляется мысль: а может можно скриптами? Немного vim или nano и код теста можно изменить прямо в консоле тестовой среды.
Размышления о скриптах в JVM рано или поздно приводят к Groovy. В свою очередь, в Groovy заложена важность выполнения тестов. Можете почитать об этом прямо в документации. Groovy cli умеет запускать тесты, написанные для различных библиотек из коробки.
Возьмем для примера тест, проверяющий доступность google.com, написанный для Junit Jupiter.
class SampleJunit5 {
@org.junit.jupiter.api.Test
void pingGoogleInJunit5() {
def address = InetAddress.getByName("google.com")
println "Pinging google from junit5 test now..."
assert address.isReachable(60000)
}
}
Настройка окружения для запуска примеров
При написании статьи использовалась версия Groovy 3.0.10.
Если интересно попробовать дальнейшее самим - установите Groovy, следуя инструкциями.
Или скопируйте репозиторий с примерами из GitHub, ветка habr-sample-tests-groovy
:
git clone --branch habr-sample-tests-groovy git@github.com:alopukhov/habr-samples.git
cd habr-samples
./gradlew setupWorkspace
cd workspace
export GROOVY_HOME="${PWD}/groovy-3.0.10/"
export PATH="$GROOVY_HOME/bin:$PATH"
Настроив окружение, запустим тест командой groovy SampleJunit5.groovy
и получим примерно такой выход (если у вас конечно доступен google.com):
Pinging google from junit5 test now...
JUnit5 launcher: passed=1, aborted=0, failed=0, skipped=0, time=109ms
Ожидаемая ситуация и с непроходящим тестом:
class FailingTest {
@org.junit.jupiter.api.Test
void thisWillFail() { assert 2 * 2 == 5 }
@org.junit.jupiter.api.Test
void thisWillPass() { assert 2 * 2 == 4 }
}
Запускаем groovy FailingTest.groovy
и получаем:
JUnit5 launcher: passed=1, aborted=0, failed=1, skipped=0, time=75ms
Failures (1):
JUnit Jupiter:FailingTest:thisWillFail()
MethodSource [className = 'FailingTest', methodName = 'thisWillFail', methodParameterTypes = '']
=> Assertion failed:
assert 2 * 2 == 5
| |
4 false
org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:436)
... и еще сотни строк стектрейса ...
Схожим образом можно запустить и код для других библиотек.
Junit 4
class SampleJunit4 {
@org.junit.Test
void pingGoogleInJunit4() {
def address = InetAddress.getByName("google.com")
println "Pinging google from junit4 test now..."
assert address.isReachable(60000)
}
}
> groovy SampleJunit4.groovy
Pinging google from junit4 test now...
JUnit 4 Runner, Tests: 1, Failures: 0, Time: 73
Spock
class SampleSpockSpeck extends spock.lang.Specification {
def "ping google in Spock"() {
expect:
println "Pinging google from spock now"
InetAddress.getByName("google.com").isReachable(60000)
}
}
Для запуска уже нужна библиотека Spock - она не входит в поставку Groovy.
Версия должна быть совместимой с версией Groovy. Подробнее можно узнать на сайте spockframework.org.
> groovy -cp ./libs/spock-core-2.1-groovy-3.0.jar SampleSpockSpeck.groovy
Pinging google from spock now
JUnit5 launcher: passed=1, aborted=0, failed=0, skipped=0, time=97ms
Для spock 1.x потребуется версия groovy 2.5.x.
Существенное различие в том, что начиная с версии 2, spock использует для запуска junit platform, в то время как раньше использовался Junit4 runner Sputnik.
TestNG
class SampleTestNG {
@org.testng.annotations.Test
void pingGoogleInTestNG() {
def address = InetAddress.getByName("google.com")
println "Pinging google now from testng"
assert address.isReachable(60000)
}
@org.testng.annotations.Test
void failInTestNg() { assert false }
}
Для запуска желательно наличие slf4j логгера, например slf4j-simple.
> groovy -cp ./libs/slf4j-simple-1.7.36.jar SampleTestNG.groovy
[main] INFO org.testng.internal.Utils - [TestNG] Running:
Command line suite
Pinging google now from testng
===============================================
Command line suite
Total tests run: 2, Passes: 1, Failures: 1, Skips: 0
===============================================
Увы, нет даже имени упавшего теста. Вероятно, можно настроить testng на более детальный вывод. Надеюсь, в комментариях подскажут как.
Отвечают за такую возможность различные реализации GroovyRunner. Так что если есть желание запускать тесты для нетипичного фреймворка - можно написать свою реализацию.
Вывод при таком запуске в целом небогатый: summary по запущенным методам и информация о падениях. Имена запущенных тестов не отображаются. А при запуске testng можно не рассчитывать даже на имена упавших тестовых методов.
Однако такие тесты - курам на смех.
Давайте посмотрим, как обстоят дела с запуском тестов со вспомогательными классами.
Создадим 2 файла:
//content of Calculator.groovy
class Calculator {
static long multiply(long a, long b) { a * b }
}
//content of SampleWithHelper.groovy
class SampleWithHelper {
@org.junit.jupiter.api.Test
void mathTest() { assert 4 == Calculator.multiply(2, 2)}
}
Вроде работает:
> groovy SampleWithHelper.groovy
groovy SampleWithHelper.groovy
JUnit5 launcher: passed=1, aborted=0, failed=0, skipped=0, time=71ms
Но если запустить с другим working directory:
> groovy ./workspace/SampleWithHelper.groovy
JUnit5 launcher: passed=0, aborted=0, failed=1, skipped=0, time=70ms
Failures (1):
JUnit Jupiter:SampleWithHelper:mathTest()
MethodSource [className = 'SampleWithHelper', methodName = 'mathTest', methodParameterTypes = '']
=> groovy.lang.MissingPropertyException: No such property: Calculator for class: SampleWithHelper
org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:65)
... и еще сотни строк стектрейса ...
Проблема в отсутствующем классе Calculator. Если изменим SampleWithHelper следующим образом:
import static Calculator.multiply
class SampleWithHelper {
@org.junit.jupiter.api.Test
void mathTest() { assert 4 == multiply(2, 2)}
}
Получим более понятное сообщение об ошибке:
> groovy ./workspace/SampleWithHelper.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
C:\Users\andre\IdeaProjects\habr_scripting_tests_example\workspace\SampleWithHelper.groovy: 1: unable to resolve class Calculator
@ line 1, column 1.
import static Calculator.multiply
^
Дело в том, что важно корректно указывать classpath. Текущая директория всегда добавляется в classpath скриптами запуска groovy, но если каталог со скриптами иной - необходимо его указать явно.
> groovy -cp ./workspace ./workspace/SampleWithHelper.groovy
JUnit5 launcher: passed=1, aborted=0, failed=0, skipped=0, time=75ms
Итак, по одному тесты запускать научились. Осталось запускать скопом.
И тут на помощь приходит JUnit platform. JUnit 5 не просто новое удобное API, но целая платформа для запуска тестов от разных фреймворков.
Немного библиотек и мы можем составить набор тестов для запуска при помощи Suite API:
import org.junit.platform.suite.api.*
@Suite
@SuiteDisplayName("Sample Suite for Habr")
@SelectClasses([SampleJunit4, SampleJunit5, SampleTestNG, SampleSpockSpeck])
class SuiteTest {
}
> groovy -cp "libs/*" ./SuiteTest.groovy
Pinging google from junit5 test now...
Pinging google from spock now
Pinging google now from testng
Pinging google from junit4 test now...
JUnit5 launcher: passed=4, aborted=0, failed=1, skipped=0, time=226ms
Failures (1):
JUnit Platform Suite:Sample Suite for Habr:TestNG:SampleTestNG:failInTestNg
MethodSource [className = 'SampleTestNG', methodName = 'failInTestNg', methodParameterTypes = '']
=> Assertion failed:
... more...
N.B. При указании wildcard classpath важно, чтобы символ *
не был раскрыт как glob pattern. В Windows просто используйте groovy.bat для запуска команд и оборачивайте пути в двоеточия.
Теперь все тесты запустились при помощи JUnit5 launcher, вдобавок даже стало видно, какой метод упал в TestNG.
Но можно лучше. В JUnit Platform есть модуль junit-platform-console
для запуска тестов из командной строки.
Простой скрипт:
//content of RunJunitConsole.groovy
org.junit.platform.console.ConsoleLauncher.main(args)
И теперь можно получить куда более детальный вывод:
> groovy -cp "libs/*" RunJunitConsole.groovy --select-class SuiteTest --disable-banner --disable-ansi-colors
Pinging google from junit4 test now...
Pinging google from junit5 test now...
Pinging google from spock now
Pinging google now from testng
.
+-- JUnit Jupiter [OK]
+-- JUnit Platform Suite [OK]
| '-- Sample Suite for Habr [OK]
| +-- JUnit Vintage [OK]
| | '-- SampleJunit4 [OK]
| | '-- pingGoogleInJunit4 [OK]
| +-- JUnit Jupiter [OK]
| | '-- SampleJunit5 [OK]
| | '-- pingGoogleInJunit5() [OK]
| +-- Spock [OK]
| | '-- SampleSpockSpeck [OK]
| | '-- ping google in Spock [OK]
| '-- TestNG [OK]
| '-- SampleTestNG [OK]
| +-- failInTestNg [X] assert false
| '-- pingGoogleInTestNG [OK]
+-- JUnit Vintage [OK]
+-- Spock [OK]
'-- TestNG [OK]
Failures (1):
JUnit Platform Suite:Sample Suite for Habr:TestNG:SampleTestNG:failInTestNg
MethodSource [className = 'SampleTestNG', methodName = 'failInTestNg', methodParameterTypes = '']
... a bit of stack trace ...
Test run finished after 229 ms
[ 14 containers found ]
[ 0 containers skipped ]
[ 14 containers started ]
[ 0 containers aborted ]
[ 14 containers successful ]
[ 0 containers failed ]
[ 5 tests found ]
[ 0 tests skipped ]
[ 5 tests started ]
[ 0 tests aborted ]
[ 4 tests successful ]
[ 1 tests failed ]
Можно запускать несколько тестов без использования Suite:
> groovy -cp "libs/*" RunJunitConsole.groovy --disable-banner --disable-ansi-colors -e junit-jupiter -c SampleJunit5 -c FailingTest
Pinging google from junit5 test now...
.
'-- JUnit Jupiter [OK]
+-- SampleJunit5 [OK]
| '-- pingGoogleInJunit5() [OK]
'-- FailingTest [OK]
+-- thisWillFail() [X] assert 2 * 2 == 5
| | |
| 4 false
'-- thisWillPass() [OK]
... failure details ...
Но самое главное можно указать директорию --reports-dir
, куда будут сгенерированы отчеты в JUnit xml формате. Эти отчеты далее можно собрать для отображения.
Единственная незадача - все имена тестов нужно указывать явно. Classpath и package scanning недоступны, т.к. существующие утилиты ожидают наличие файлов с классами.
Однако, немного поработав напильником, можно написать свой TestEngine.
Так, с некоторыми оговорками получилось написать Sybok Test Engine, позволяющий использовать поиск тестов в директории и запускать тесты без помощи полного дистрибутива groovy.
> java -cp ".\libs\*;.\sybok-libs\*" org.junit.platform.console.ConsoleLauncher --disable-banner --disable-ansi-colors -e sybok-engine --config sybok.script-roots=. --config sybok.delegate-engine-ids=spock,junit-jupiter,junit-vintage -d . -n ".*"
Pinging google from spock now
Pinging google from junit4 test now...
Pinging google from junit5 test now...
.
'-- Sybok [OK]
+-- Spock [OK]
| '-- SampleSpockSpeck [OK]
| '-- ping google in Spock [OK]
+-- JUnit Vintage [OK]
| '-- SampleJunit4 [OK]
| '-- pingGoogleInJunit4 [OK]
'-- JUnit Jupiter [OK]
+-- SampleJunit5 [OK]
| '-- pingGoogleInJunit5() [OK]
+-- FailingTest [OK]
| +-- thisWillFail() [X] assert 2 * 2 == 5
| | | |
| | 4 false
| '-- thisWillPass() [OK]
'-- SampleWithHelper [OK]
'-- mathTest() [OK]
... failure details ...
Он (пока) не может запускать TestNG классы, но думаю это поправимо.
А если еще немного постараться, можно будет заставить запускаться тесты, написанные на другом скриптовом языке под JVM. Например, Kotlin script.
Надеюсь, статья была полезной. Расскажите в комментариях: что бы вы использовали для запуска тестов на JVM?