Статьи

Тестирование JVM на стороне сервера JavaScript с Жасмин, Спок и Нашорн

Использование JavaScript не ограничивается клиентским кодом в браузере или серверным кодом на основе NodeJS . Многие проекты на основе JVM используют его как внутренний язык сценариев. Тестирование такого рода функциональности не является ни простым, ни стандартным. В этом посте я намерен продемонстрировать подход для тестирования JavaScript в серверной среде JVM с использованием зрелых инструментов, таких как Jasmine , Spock и Nashorn .

Использование JavaScript в качестве движка сценариев внутри приложения JVM существенно отличается от кодирования на стороне клиента. И, к сожалению, в настоящее время нет промышленных стандартных инструментов для его тестирования.

Что касается существующих подходов, найденных в Интернете, я бы хотел выделить следующие недостатки:

  • отсутствие интеграции со средствами сборки и непрерывной интеграции (Maven, Gradle, Jenkins и т. д.)
  • недостаточное сотрудничество с IDE
    • нет возможности запустить один комплект или тест из IDE
    • невозможно просмотреть отчеты о выполнении теста из IDE
  • тесная связь с браузерной средой
  • нет возможности использования пользовательских исполнителей JavaScript

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

Подобные подходы обычно имеют схожие недостатки:

  • трудно что-то заглушить или посмеяться в коде JS, обычно заканчивая взломом prototype JS
  • нужно слишком много оркестровки для насмешливой среды для сценария
  • трудно организовать тесты в наборы и сообщать об ошибках выполнения тестов
  • предыдущие причины создание пользовательских рамок тестового набора для конкретного проекта
  • не используя существующие инструменты и инфраструктуры тестирования JavaScript

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

  • Jasmine является одним из самых известных инструментов TDD / BDD для JavaScript
  • Spock — отличный фреймворк для тестирования JVM на базе Junit и Groovy
  • Nashorn — это современный скриптовый движок, представленный в JDK8.

Индивидуальный JavaScript-бегун (на основе Nashorn)

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

Давайте рассмотрим, что у нас есть такой настраиваемый бегун, который принимает имя сценария и карту предопределенных переменных в качестве параметров и возвращает полученное значение выполненного сценария.

JavaScriptRunner.java

1
2
3
4
5
6
7
8
public class JavaScriptRunner {
  public static Object run(String script, Map<String, Object> params) throws Exception {
    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine engine = factory.getEngineByName("nashorn");
    engine.getBindings(ScriptContext.ENGINE_SCOPE).putAll(params);
    return engine.eval(new InputStreamReader(JavaScriptRunner.class.getResourceAsStream(script))); (1)
  }
}
1 источник сценария ищется в classpath.

Настройка Жасмин

Чтобы начать использовать фреймворк Jasmine нам нужно:

  • скачайте Jasmine и распакуйте его в папку /jasmine/jasmine-2.1.2 каталоге ресурсов проекта
  • пользовательский скрипт начальной загрузки, поскольку Jasmine не поддерживает платформы на основе JVM

jasmine2-bootstrap.js

01
02
03
04
05
06
07
08
09
10
11
12
13
14
var loadFromClassPath = function(path) { (1)
  load(Java.type("ua.eshepelyuk.blog.nashorn.Jasmine2Specification").class.getResource(path).toExternalForm());
};
 
var window = this;
 
loadFromClassPath("/jasmine/jasmine-2.1.2/jasmine.js");
loadFromClassPath("/jasmine/jasmine2-html-stub.js"); (2)
loadFromClassPath("/jasmine/jasmine-2.1.2/boot.js");
load({script: __jasmineSpec__, name: __jasmineSpecName__}); (3)
 
onload(); (4)
 
jsApiReporter.specs(); (5)
1 вспомогательная функция, разрешающая путь к сценарию из местоположения пути к классу.
2 Специфичный для Nashorn код, корректирующий Jasmine для не браузерных сред. Не является частью распространения Jasmine .
3 загрузка исходного кода набора тестов, подробности см. в следующем разделе.
4 фальшивое событие load браузера, которое должно запустить выполнение набора тестов.
5 это значение будет возвращено как результат скрипта.

Преобразуйте отчет Жасмин в тесты Спока

Имея JS executor и скрипт начальной загрузки для Jasmine мы могли бы создать тест JUnit чтобы перебрать результаты набора и проверить, все ли успешно. Но это станет кошмаром, чтобы понять, какой именно тест провалился и в чем причина неудачи. Нам действительно хотелось бы иметь возможность представлять каждую спецификацию Jasmine как тест JUnit , чтобы любой инструмент Java мог подобрать и проверить результаты. Вот почему Spock может быть ответом на проблему с помощью Data Driven Testing, который позволяет разработчику объявлять список входных данных и для каждого элемента этого набора данных будет создаваться и выполняться новый тест. Это очень похоже на Parametrized Test of Junit но гораздо более мощную реализацию.

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

  • если статус находится в состоянии pending или passed , это означает, что спецификация либо игнорируется, либо успешна
  • в противном случае тест Spock должен выдать ошибку подтверждения, заполнив исключение подтверждения, заполненное сообщениями о сбоях, сообщенными Jasmine

Jasmine2Specification.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract class Jasmine2Specification extends Specification {
  @Shared def jasmineResults
 
  def setupSpec() {
    def scriptParams = [
        "__jasmineSpec__"    : getMetaClass().getMetaProperty("SPEC").getProperty(null), (1)
        "__jasmineSpecName__": "${this.class.simpleName}.groovy"
    ]
    jasmineResults = JavaScriptRunner.run("/jasmine/jasmine2-bootstrap.js", scriptParams) (2)
  }
 
  def isPassed(def specRes) {specRes.status == "passed" || specRes.status == "pending"}
 
  def specErrorMsg(def specResult) {
    specResult.failedExpectations
        .collect {it.value}.collect {it.stack}.join("\n\n\n")
  }
 
  @Unroll def '#specName'() {
    expect:
      assert isPassed(item), specErrorMsg(item) (3)
    where:
      item << jasmineResults.collect { it.value }
      specName = (item.status != "pending" ? item.fullName : "IGNORED: $item.fullName") (4)
  }
}
1 предоставление исходного кода набора Jasmine как переменной jasmineSpec , доступной для исполнителя JS.
2 фактическое исполнение Jasmine свиты.
3 для каждого результата набора мы assert он либо был успешным, выдавая ошибку подтверждения с сообщением, созданным Жасмином, при ошибке.
4 дополнительная переменная поставщика данных для выделения игнорируемых тестов.

Полный пример

Давайте создадим набор тестов для простой функции JavaScript.

mathUtils.js

1
2
3
var add = function add(a, b) {
  return a + b;
};

Используя базовый класс из предыдущего шага, мы могли бы создать пакет Spock содержащий тесты JavaScript. Чтобы продемонстрировать все возможности нашего решения, мы создадим успешный, неудачный и проигнорированный тест.

MathUtilsTest.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class MathUtilsTest extends Jasmine2Specification {
    static def SPEC = """ (1)
loadFromClassPath("/js/mathUtils.js"); (2)
describe("suite 1", function() {
  it("should pass", function() {
    expect(add(1, 2)).toBe(3);
  });
  it("should fail", function() {
    expect(add(1, 2)).toBe(3);
    expect(add(1, 2)).toBe(0);
  });
  xit("should be ignored", function() {
    expect(add(1, 2)).toBe(3);
  });
})
"""
}
1 Фактический код набора Jasmine представлен в виде String переменной.
2 загрузка тестируемого модуля с использованием функции, унаследованной от jasmine-bootstrap.js .

Рисунок 1. Результаты теста от IntelliJ IDEA

Рисунок 1. Результаты теста от IntelliJ IDEA

Инъекция языка IntelliJ Idea

Несмотря на то, что этот микро-фреймворк должен работать во всех IDE, его наиболее удобно использовать в IntelliJ IDEA благодаря внедрению языка . Функция позволяет встраивать произвольный язык в файл, созданный на другом языке программирования. Таким образом, мы можем встроить блок кода JavaScript в спецификацию Spock написанную на Groovy.

Рисунок 2. Внедрение языка

Рисунок 2. Внедрение языка

Плюсы и минусы решения

преимущества

  • использование стандартных инструментов тестирования для обоих языков
  • бесшовная интеграция с инструментами сборки и инструментами непрерывной интеграции
  • возможность запуска одного пакета из IDE
  • запускать одиночный тест из конкретного пакета благодаря целенаправленной функции Jasmine

Недостатки

  • нет чистого способа обнаружения конкретной строки исходного кода в случае исключения теста
  • немного IntelliJ IDEA ориентированная установка

PS

Для этого примера проекта я использовал современный движок Nashorn от JDK8. Но на самом деле нет никаких ограничений на это. Тот же подход был успешно применен для проектов, использующих старый движок Rhino . И опять же, Jasmine — это мое личное предпочтение. Благодаря дополнительному QUnit коду можно настроить использование Mocha , QUnit и так далее.

  • Полный код проекта доступен на My GitHub .