Статьи

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

В предыдущем посте я представил мышление о (визуальных) компонентах и ​​использовал обозреватель задач в качестве примера «компонента» пользовательского интерфейса.

Grails-проблемно-обзор-главный экран-каркасный

Я объяснил, что с помощью

  • Просмотр моделей, например простых старых объектов Groovy (POGO), содержащих связанные данные, например, class TaskBrowser
  • Библиотеки тегов ( LayoutTagLib ) и теги ( def taskBrowser ) для отображения связанного HTML-кода ( views/layouts/components/_taskBrowser.gsp ) на странице

позволяет создавать более понятный и проверяемый код.

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

Части

Так что это (упрощенные) части в уравнении.

Задача — Доменный класс

1
2
3
4
5
6
class Task {
  enum Type { PERSONAL, WORK }
 
  String title
  Type type
}

TaskBrowser — просто POGO с данными

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class TaskBrowser {
 
  List tasks = []
 
  /**
   * Month to start with.
   *
   * @return number between 1 and 12
   */
  int getStartMonth() {
    def nowDate = new Date()
    nowDate[Calendar.MONTH] + 1
  }
}

HomeController — Создание обозревателя задач в действии index .

1
2
3
4
5
6
7
class HomeController {
  def taskService
 
  def index() {
    [taskBrowser: new TaskBrowser(tasks: taskService.retrieveTasks())]
  }
}

home / index.gsp — ВСП для действия index

01
02
03
04
05
06
07
08
09
10
11
<!doctype html>
<html>
 <head>
 <meta name="layout" content="main" />
 <title>Tasks</title>
 </head>
 
 <body>
 <g:taskBrowser taskBrowser="${taskBrowser}"/>
 </body>
</html>

views / layouts / components / _taskBrowser.gsp — HTML- код браузера задач

1
2
3
4
<div class="row month-${startMonth}" id="task-browser">
<div class="six columns">
  <g:if test="${tasks}">
  <%-- 500 lines more... --%>

LayoutTagLib — наконец, библиотека тегов

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
/**
 * Renders the task browser.
 *
 * @attr taskBrowser REQUIRED a task browser instance
 * @attr filter Optionally a {@link Task.Type} to show only those tasks
 */
def taskBrowser = { attrs ->
  if (!attrs.taskBrowser) {
    throwTagError("Tag [taskBrowser] is missing " +
                    "required attribute [taskBrowser]")
  }
 
  TaskBrowser browser = attrs.taskBrowser
 
  // filter tasks by type
  def tasks = browser.tasks
  if (attrs.filter && attrs.filter instanceof Task.Type) {
    tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
  }
 
  out << render(template: '/layouts/components/taskBrowser',
    model: [tasks : tasks,
      months : browser.months,
      startMonth : browser.startMonth
  ])
}

Модульный тест

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

Обычно, когда вы используете команду Grails для создания контроллера, службы или библиотеки тегов, также создается связанный модульный тест. Если нет, вы всегда можете создать его позже с

1
grails create-unit-test LayoutTagLib

В любом случае, мы начинаем с LayoutTagLibSpec который изначально довольно пуст.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import grails.test.mixin.TestFor
import spock.lang.Specification
 
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 def setup() {
 }
 
 def cleanup() {
 }
 
 void "test something"() {
 expect:"fix me"
 true == false
 }
}

Аннотация @TestFor является частью инфраструктуры Grails. Он не только указывает на тестируемый класс (то есть на фактический модуль, который мы должны здесь тестировать), но также дает нам конкретный экземпляр этого класса.

Подробнее об этом позже.

Теперь мы можем реализовать наш первый тест под названием…

Тег браузера задач должен показывать все задачи по умолчанию

Несмотря на то, что скелетный метод "test something" начинается с «test…», я пытаюсь пропустить эту часть. Очевидно, мы создаем тесты, и повторение «test xxx» не имеет никакого дополнительного значения, но занимает много места.

Если бы мы были в модульном тесте TaskBrowser (например, TaskBrowserSpec ), я бы пропустил имя тестируемого класса из метода test, например: « тег браузера задач должен показать… ». Так как мы находимся в более общем LayoutTagLib я хотел бы знать, о каком теге — о многих других, конечно, мы говорим, поэтому я начну с «тега браузера задач…»

Я обычно начинаю с размещения меток Given / When / Then Spock в методе тестирования. Это помогает мне структурировать свою собственную голову, думая о

  1. Каковы предпосылки? (Дано)
  2. Какой актуальный код для вызова? (Когда)
  3. Что там утверждать или проверять? (Потом)

Вот что у меня сейчас:

01
02
03
04
05
06
07
08
09
10
11
12
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
 
   given:
 
   when:
 
   then:
 }
}

Фактический вызов тега находится в разделе Когда . Поскольку аннотация @TestFor указывает на класс библиотеки тегов, нам дается неявная переменная tagLib для работы, которая каждый раз ссылается на чистый экземпляр LayoutTagLib .

Не делайте этого в модульном тесте, потому что это предварительноTestFor способ:

1
2
3
def layoutTagLib = new LayoutTagLib()
// or <span class="pl-k">def</span> layoutTagLib <span class="pl-k">=</span> applicationContext<span class="pl-k">.</span>getBean(Layout<span class="pl-k">TagLib</span>)
layoutTagLib.taskBrowser(...)

но используйте tagLib , Люк.

1
tagLib.taskBrowser(...)

Итак, у нас есть это:

01
02
03
04
05
06
07
08
09
10
11
12
13
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
 
   given:
 
   when:
   tagLib.taskBrowser()
 
   then:
 }
}

Я знаю, что потоку TaskBrowser -Path для этого теста нужен экземпляр TaskBrowser . Должна быть хотя бы одна задача, чтобы убедиться, что тег показывает его по умолчанию. Итак, давайте добавим их:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
 given:
 def task = new Task(title: "My task")
 def browser = new TaskBrowser(tasks: [task])
 
 when:
 tagLib.taskBrowser(taskBrowser: browser)
 
 then:
 true
 }
}

Эй, почему это true в блоке Тогда ? Это потому, что нам нужно иметь блок Then после When , чтобы иметь возможность выполнить этот тест один раз в данный момент. Обычно мы пишем это, вероятно, с Expect as

01
02
03
04
05
06
07
08
09
10
11
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
 
   given:
 
   expect:
 
 }
}

но я слишком ленив, чтобы обновить его до « Ожидать» и позже изменить его на « Когда / Тогда» & # 56841; Тогда нам все равно понадобится потом. В модульном тесте для библиотеки тегов Grails выполняет рендеринг шаблона /layouts/components/_taskBrowser.gsp с использованием предоставленной модели.

Помните код для LayoutTagLib ?

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
/**
 * Renders the task browser.
 *
 * @attr taskBrowser REQUIRED a task browser instance
 * @attr filter Optionally a {@link Task.Type} to show only those tasks
 */
def taskBrowser = { attrs ->
  if (!attrs.taskBrowser) {
    throwTagError("Tag [taskBrowser] is missing " +
                    "required attribute [taskBrowser]")
  }
 
  TaskBrowser browser = attrs.taskBrowser
 
  // filter tasks by type
  def tasks = browser.tasks
  if (attrs.filter && attrs.filter instanceof Task.Type) {
    tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
  }
 
  out << render(template: '/layouts/components/taskBrowser',
    model: [tasks : tasks,
      months : browser.months,
      startMonth : browser.startMonth
  ])
}

Если вышеуказанный (упрощенный) тест завершается успешно, _taskBrowser.gsp — и его логика — не сбои с исключением. Вы можете сделать опечатку в своем шаблоне (шаблонах) и увидеть, что оценка не удалась. Может быть, стоит рассмотреть только оценку GSP, но мы ничего не проверили.

Как мы узнаем, что на правильный шаблон ссылаются? Как мы узнаем, действительно ли передана правильная модель?

Неизбежная правда

Если вы посмотрите на главу «Тестирование» документации Grails, то увидите упрощенный пример тестирования ответа SimpleTagLib

1
2
3
4
5
6
class SimpleTagLib {
  static namespace = 's'
 
  def hello = { attrs, body ->
    out << "Hello ${attrs.name ?: 'World'}"
  }
1
2
3
4
5
6
@TestFor(SimpleTagLib)
class SimpleTagLibSpec extends Specification {
 
  void "test tag calls"() {
    expect:
    tagLib.hello().toString() == 'Hello World'

Наш тег не так прост, как возвращение «Hello World» — наш тег отображает 500 строк HTML-кода браузера сложных задач в выходной буфер. Проверить это простым способом не так просто.

Здесь есть несколько подходов.

# 1 — Проверка деталей

Содержимое часто используется в частях кода, которые просто необходимо проверить на наличие элемента. Мы могли бы попытаться проверить существование нашей единственной задачи «Моя задача» где-нибудь в выводе следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
   given:
   def task = new Task(title: "My task")
   def browser = new TaskBrowser(tasks: [task])
 
   when:
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()
 
   then:
   result.contains "My task"
 }
}

Мы успешно убедились, что «Моя задача» видна где-то в 500 строках вывода.

(Psst, быстро усилить тест, прежде чем мы продолжим — убедитесь, что у нас всегда есть как минимум несколько тестовых элементов для проверки логики, которая имеет дело с коллекцией, вместо одного элемента)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
   given:
   def task1 = new Task(title: "My 1st task")
   def task2 = new Task(title: "My 2nd task")
   def browser = new TaskBrowser(tasks: [task1, task2])
 
   when:
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()
 
   then:
   result.contains "My 1st task"
   result.contains "My 2nd task"
 }
}

Недостатком является то, что мы связали наш тест для логики тегов lib (показ или фильтрация задач) с рендерингом HTML (наличие заголовка задачи)

Чтобы немного смягчить это, мы должны быть …

# 2 — Управление тем, какие части отображаются

Как и в случае с тестами Controller, мы можем использовать функцию ControllerUnitTestMixin для макета представления, используемого для рендеринга.

Используйте неявные getViews() или getGroovyPages() — которые возвращают Map для манипулирования нами. Замените реальный шаблон нашим собственным контентом, в котором мы контролируем, что и как визуализируется модель.

Сначала убедитесь, что мы действительно перезаписали правильный путь к шаблону, позволив тесту провалиться. Тег taskBrowser говорит, что render(template: '/layouts/components/taskBrowser'... поэтому мы должны поместить альтернативное содержимое в ключ '/layouts/components/_taskBrowser.gsp' — существует несоответствие в формате записи пути к шаблону ,

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
   given:
   def task1 = new Task(title: "My 1st task")
   def task2 = new Task(title: "My 2nd task")
   def browser = new TaskBrowser(tasks: [task1, task2])
 
   when:
   views['/layouts/components/_taskBrowser.gsp'] = 'bogus'
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()
 
   then:
   result.contains "My 1st task"
   result.contains "My 2nd task"
 }
}

Это не правильно …

1
2
3
4
5
Condition not satisfied:
 
result.contains "My 1st task"
|      |
bogus  false

… поэтому мы знаем, что у нас есть правильный ключ.

Теперь выберите правильное содержание.

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

1
2
3
4
5
6
Condition not satisfied:
 
result.contains "My 1st task"
|      |
|      false
[sample.Task : (unsaved), sample.Task : (unsaved)]

Dang!

Мы не можем (и не должны) полагаться на представление String класса Task, нашей модели.

Не добавляйте метод toString() ! Мы могли бы настроить тест для обнаружения наличия правильных элементов по уникальному свойству, например по названию .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
 
 void "task browser tag should show all tasks by default"() {
   given:
   def task1 = new Task(title: "My 1st task")
   def task2 = new Task(title: "My 2nd task")
   def browser = new TaskBrowser(tasks: [task1, task2])
 
   when:
   views['/layouts/components/_taskBrowser.gsp'] = '${tasks.title}'
   // make sure only titles are rendered e.g. [My 1st task, My 2nd task]
   def result = tagLib.taskBrowser(taskBrowser: browser).toString()
 
   then:
   result.contains "My 1st task"
   result.contains "My 2nd task"
 }
}

Это удается!

И некрасиво. Мы по-прежнему полагаемся на приведение результата рендеринга ( StreamCharBuffer ) к StreamCharBuffer которую необходимо сравнить в целом с тем, что мы ожидаем, или проверить на наличие частей. Для тестирования некоторых небольших фрагментов HTML это хорошо.

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

Промыть и повторить

Я часто беру первый простой тест за основу для дальнейших тестов. Во втором тесте нам нужно проверить, может ли браузер задач на самом деле фильтровать один раз по типам задач, таким как Личная или Рабочая .

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
void "task browser tag should show only personal tasks"() {
 given:
 def task1 = new Task(title: "My 1st task", type: Type.PERSONAL)
 def task2 = new Task(title: "My 2nd task", type: Type.WORK)
 def browser = new TaskBrowser(tasks: [task1, task2])
 
 and:
 def filterType = Type.PERSONAL
 
 when:
 views['/layouts/components/_taskBrowser.gsp'] = '${tasks.title}'
 def result = tagLib.taskBrowser(taskBrowser: browser,
 filter: filterType).toString()
 
 then:
 result.contains "My 1st task"
 !result.contains("My 2nd task")
}

(Обычно я бы поставлял все виды меток для блоков given: when: и т.д., но я оставлю это как упражнение для читателя.)

Если бы я не слишком устал проводить выходные с римлянами и гладиаторами, я написал бы еще несколько вариантов и заключительную часть, но я надеюсь, что просто запись выше здесь, в блоге Теда Винке, поможет кому-то.

Удачного тестирования!