Статьи

GrUnit: Groovy способ юнит-тестирования

Есть продолжение этой статьи сейчас

Мы, Java / Groovy разработчики, постоянно пишем юнит-тесты. Я считаю, что нет необходимости убеждать кого-либо, почему это так важно. Моя цель сегодня — показать небольшой, но удобный экспериментальный инструмент, включенный в Groovy ++ (с возможностью переноса в Groovy Core). Вы можете найти больше информации о GrUnit и Groovy ++ на домашней странице проекта Groovy ++

Так что же такое GrUnit?

GrUnit — это преобразование Groovy AST (абстрактное синтаксическое дерево), которое допускает только две вещи

  • написание тестов JUnit немного менее многословно
  • выполнение тестов в виде скриптов Groovy

Давайте начнем с примера

@Typed package org.mbte.groovypp.compiler

import org.codehaus.groovy.tools.FileSystemCompiler
import org.codehaus.groovy.control.CompilerConfiguration

def finder = new FileNameFinder ()

String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*Test.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)

Похоже на обычный Groovy скрипт, верно? И это обычный Groovy скрипт. Но в то же время это тестовый пример JUnit с одним тестом. Вот вывод после выполнения скрипта. 

.
Time: 3.558

OK (1 test)

Образованный читатель может заметить, что это нормальный вывод junit.textui.TestRunner. Это правильно. Наш класс расширяет блестящий GroovyTestCase, а основной метод класса просто вызывает тестовый бегун. Конечно, тело нашего скрипта становится телом единственного теста в тестовом примере.

Единственное, что нам нужно сделать, это поместить скрипт в файл с расширением .grunit.

Как нам этого добиться? Магия?

Конечно, в этом нет никакой магии. Используемая нами технология (чрезвычайно мощная и очень популярная в мире Groovy) — преобразование AST во время компиляции. Преобразование AST — это возможность расширить поведение компилятора с помощью плагинов, которые на некоторых этапах компиляции изменяют внутреннее представление кода.

Преобразования мы делаем в нашем случае

  • наш класс скрипта должен расширять GroovyTestCase
  • Основной метод должен вызывать junit.textui.TestRunner
  • тело скрипта должно быть преобразовано в публичный метод void testXXX ()

Теперь давайте добавим еще один тест в наш скрипт. Это можно сделать очень просто, добавив вызов testXXX {…} в любом месте верхнего уровня нашего скрипта.

testTestsExists {
assertTrue new File("./StdLibTest/tests/").exists()
}

def finder = new FileNameFinder ()

String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)

И вуаля! Вывод нашего скрипта теперь выглядит иначе — у нас запущены два теста

..
Time: 3.372

OK (2 tests)

Ты хочешь больше? Наше удовольствие

testTestsExists {
assertTrue new File("./StdLibTest/tests/").exists()
}

testSuperclassName {
assertEquals "groovy.util.GroovyTestCase", this.class.superclass.name
}

def finder = new FileNameFinder ()

String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)

Который выполняет уже 3 теста

...
Time: 3.642

OK (3 tests)

Может быть, вы хотите использовать какой-то другой базовый класс для своего теста? Нет проблем. Просто добавьте extendsTest XXX на верхний уровень скрипта

extendsTest GroovyShellTestCase

testTestsExists {
assertTrue new File("./StdLibTest/tests/").exists()
}

testInstance {
assertEquals "groovy.util.GroovyShellTestCase", this.class.superclass.name
}

def finder = new FileNameFinder ()

String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)

И конечно setUp () / tearDown () также поддерживаются. Опять же, нам просто нужно добавить его где-нибудь на верхнем уровне нашего скрипта. Приведенный ниже код является одним из наиболее важных тестов в кодовой базе Groovy ++ — мы проверяем, что мы можем скомпилировать нашу стандартную библиотеку

extendsTest GroovyFileSystemCompilerTestCase

@Field FileNameFinder finder

setUp { finder = new FileNameFinder () }

tearDown { finder = null }

testFinderNotNull { assertNotNull finder }

testCompilerNotNull { assertNotNull compiler }

def names = finder.getFileNames("./StdLib/src/", "**/*.groovy")
names.addAll(finder.getFileNames("./StdLib/src/", "**/*.java"))
compiler.compile (names as String[])

Но, пожалуй, самая интересная часть истории в том, что очень часто вам не нужен setUp / tearDown. Перепишем тест выше, используя так называемые накопленные тесты.

extendsTest GroovyFileSystemCompilerTestCase

testCompilerNotNull { assertNotNull compiler }

def srcDir = "./StdLib/src/"
testSrcExists {
assertTrue new File(srcDir).exists()
}

def finder = new FileNameFinder()
checkNamesNonEmpty {
def names = finder.getFileNames(srcDir, "**/*.groovy")
names.addAll(finder.getFileNames(srcDir, "**/*.java"))
assertFalse names.empty
}

compiler.compile (names as String[])

Накопленные тесты — это способ смешивать код скрипта с тестами testXXX {…}. Весь код сценария перед оператором testXXX {} ​​будет включен в тело вновь сгенерированного метода тестирования. Мы также можем использовать chackXXX {…} вместо testXXX {…}. Это также приведет к созданию нового метода тестирования, но код закрытия внутри checkXXX будет накапливаться для использования в последующих тестах.

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

class CompileStdLibTest extends GroovyFileSystemCompilerTestCase {
void testCompilerNotNull () {
assertNotNull compiler
}

void testSrcExist () {
def srcDir = "./StdLib/src/"
assertTrue new File(srcDir).exists()
}

void testNamesNonEmpty () {
def srcDir = "./StdLib/src/"
def finder = new FileNameFinder ()

def names = finder.getFileNames(srcDir, "**/*.groovy")
names.addAll(finder.getFileNames(srcDir, "**/*.java"))

assertFalse names.empty
}

void test$main {
def srcDir = "./StdLib/src/"
def finder = new FileNameFinder ()

def names = finder.getFileNames(srcDir, "**/*.groovy")
names.addAll(finder.getFileNames(srcDir, "**/*.java"))

assertFalse names.empty

compiler.compile(names as String[])
}

static void main (String [] args) {
junit.textui.TestRunner.run(CompileStdLibTest)
}
}

В конце я хочу заметить, что в GrUnit нет ничего специфичного для Groovy ++ (за исключением того, что оно написано на Groovy ++ и включено в дистрибутив Groovy ++), поэтому его можно легко перенести в Groovy Core, если в какой-то момент мы решим, что это имеет смысл ,

Я надеюсь, что это не было скучно. Спасибо за чтение и до следующего раза.