Изучение Grails: Tестирование приложений Grails

Исправление ошибок и создание исполняемой документации

Использование Grails снижает риск появления ошибок в первоначальной и последующих версиях вашего Web-приложения. Кроме того, написанный тестовый код можно использовать для создания подробной "исполняемой" документации, которая никогда не устареет. В этом выпуске эксперт по Grails Скотт Дэвис рассказывает о приемах тестирования приложений на основе Grails.

Скотт Дэвис, главный редактор, AboutGroovy.com

Скотт Дэвис (Scott Davis) является международно признанным автором, лектором и разработчиком программного обеспечения. Среди его книг: Groovy Recipes: Greasing the Wheels of Java, GIS for Web Developers: Adding Where to Your Application, The Google Maps API и JBoss At Work.



13.10.2010

Я горячий сторонник принципа разработки программного обеспечения через тестирование (test-driven development – TDD). Нил Форд (Neal Ford, автор книги "The Productive Programmer"), утверждает, что "написание непротестированного кода является примером профессиональной безответственности" (см. раздел Ресурсы). Майкл Физерс (Michael Feathers, автор книги "Working Effectively with Legacy Code"), определяет устаревший код (legacy code) как любой код, для которого не существует тестов. Из этого следует, что написание кода без тестов является старомодной практикой. Лично я не устаю утверждать, что любой проект должен содержать две строки тестового кода на каждую строку кода, который будет отправлен в эксплуатацию.

В статьях серии Изучение Grails пока не затрагивалась тема TDD, поскольку основное внимание уделялось способам использования базовой функциональности Grails. В тестировании кода инфраструктуры (т.е. кода, написанного не вами) также есть определенные преимущества, но на практике мне редко приходится этим заниматься. Я уверен, что Grails корректно сериализует мои объекты POGO в XML, а также сохранит объект Trip в базе данных при вызове trip.save(). В основном необходимость в тестировании возникает при проверке работоспособности вашего собственного кода. При реализации сложного алгоритма необходимо создать один или несколько юнит-тестов, чтобы гарантировать, что алгоритм работает именно так, как от него требуется. В этой статье рассказывается о том, какие средства предоставляет Grails для создания тестового кода для ваших приложений.

Ваш первый тест

Об этой серии

Grails – это инфраструктура разработки Web-приложений, сочетающая использование знакомых Java™-технологий, таких как Spring и Hibernate, с такими современными походами как, например, "соглашения по конфигурации" (convention over configuration). Grails написан на Groovy, обеспечивая бесшовную интеграцию с Java-кодом, добавляя в то же время гибкость и динамизм скриптового языка. Освоив Grails, вы навсегда измените свою точку зрения на разработку Web-приложений.

Рассмотрение вопросов тестирования мы начнем с создания нового класса модели. Этот класс будет реализовывать определенную функциональность, но его нельзя ввести в эксплуатацию без проведения тестов. Выполните команду grails create-domain-class HotelStay, как показано в листинге 1.

Листинг 1. Создание класса HotelStay
$ grails create-domain-class HotelStay

Environment set to development
     [copy] Copying 1 file to /src/trip-planner2/grails-app/domain
Created Domain Class for HotelStay
     [copy] Copying 1 file to /src/trip-planner2/test/integration
Created Tests for HotelStay

Как видно из листинга 1, в ответ на эту команду Grails создаст пустой класс модели в директории grails-app/domain. Также в директории test/integration будет создан класс GroovyTestCase с пустым методом testSomething() (ниже я объясню разницу между юнит- и интеграционными тестами). Пустой класс HotelStay со сгенерированным тестом показан в листинге 2.

Листинг 2. Пустой класс и автоматически созданный тест
class HotelStay {
}

class HotelStayTests extends GroovyTestCase {
    void testSomething() {
    }
}

Класс GroovyTestCase представляет собой тонкую Groovy-обертку над юнит-тестами JUnit 3.x. Если вы знакомы с классом TestCase в JUnit, то можно считать, что вы уже знаете, как работает GroovyTestCase. Так или иначе, тестирование заключается в контроле того, что код делает именно то, что он должен делать. Это делается программным образом при помощи различных контролирующих методов, предоставляемых JUnit, в частности assertEquals, assertTrue,assertNull и т.д.

Почему JUnit 3.x, а не 4.x?

Класс GroovyTestCase является наследником TestCase из JUnit 3.x по историческим причинам. Первая версия Groovy, выпущенная в январе 2007 г., поддерживала языковые конструкции Java 1.4. Несмотря на то что приложения могут быть запущены под JVM версий 1.4, 1.5 и 1.6, совместимость на уровне синтаксиса языка осталась на уровне Java 1.4.

Следующим крупным релизом Groovy стала версия 1.5, выпущенная в январе 2008 г. В ней поддерживаются все языковые конструкции 1.5, в том числе generics, статические импорты, циклы for/in, а также аннотации (именно они наиболее важны в контексте JUnit). Тем не менее приложения на основе Groovy 1.5 по-прежнему выполняются в JVM 1.4. Разработчики Groovy обещали, что все версии Groovy 1.x будут обратно совместимы с Java 1.4. Это требование, скорее всего, будет упразднено с выходом Groovy 2.0, который ожидается в конце 2009 или в 2010 г.

Какое же отношение все это имеет к версии классов JUnit, лежащих в основе GroovyTestCase? В JUnit 4.x используются аннотации @test, @before и @after. Эти возможности, несомненно, представляют интерес, но из соображений обратной совместимости с Java 1.4 в качестве основы для GroovyTestCase был по-прежнему выбран JUnit 3.x.

Однако ничто не запрещает вам использовать JUnit 4.x (в разделе Ресурсы приведена ссылка на документацию Groovy по этой теме). Точно также вы можете применять другие инфраструктуры для тестирования, использующие возможности Java 5 (в разделе Ресурсы приведен пример работы с TestNG в Groovy). Groovy совместим с Java на уровне байт-кода, поэтому вы можете использовать те же средства для тестирования в Groovy, что и в Java.

Добавьте содержимое листинга 3 в файлы grails-app/domain/HotelStay.groovy и test/integration/HotelStayTests.groovy.

Листинг 3. Пример простого теста
class HotelStay{
  String hotel
}

class HotelStayTests extends GroovyTestCase {
  void testSomething(){
    HotelStay hs = new HotelStay(hotel:"Sheraton")
    assertEquals "Sheraton", hs.hotel
  }
}

Код в листинге 3 представляет собой пример низкоуровневого тестирования самой инфраструктуры Grails, о котором было упомянуто выше. Можно смело считать, что подобные действия Grails выполняет корректно, поэтому данный тест достаточно бессмыслен. Тем не менее он полезен как пример простейшего для создания и запуска теста.

Для запуска всех тестов выполните команду grails test-app, а для запуска конкретного теста – grails test-app HotelStay (благодаря соглашениям по конфигурированию, можно опустить окончание Tests). Какую бы команду вы ни выполнили, вы должны увидеть в окне командной строки результат, показанный в листинге 4 (я удалил ненужные подробности, чтобы подчеркнуть важные моменты в выводе).

Листинг 4. Информация, выводимая при запуске теста
$ grails test-app
Environment set to test

No tests found in test/unit to execute ...

-------------------------------------------------------
Running 1 Integration Test...
Running test HotelStayTests...
                    testSomething...SUCCESS
Integration Tests Completed in 253ms
-------------------------------------------------------

Tests passed. View reports in /src/trip-planner2/test/reports

Важными являются четыре следующих момента.

  1. Как видите, используется тестовая среда (environment). Это означает, что используются настройки базы данных, указанные в секции test в файле conf/DataSource.groovy.
  2. Делается попытка запустить скрипты в директории test/unit. Неудивительно, что ничего не запускается, поскольку вы еще не написали ни одного тестового скрипта.
  3. Запускаются скрипты в директории test/integration. Можно увидеть вывод скрипта HotelStayTests.groovy, а также рядом признак успешности выполнения (SUCCESS – прописными буквами).
  4. В выводе присутствует указатель на набор отчетов.

Открыв файл /src/trip-planner2/test/reports/html/index.html в Web-браузере, можно увидеть отчет о запусках всех тестов (рисунок 1).

Рисунок 1. Итоговый отчет JUnit
Итоговый отчет JUnit

Нажав на ссылку HotelStayTests, вы увидите результаты теста doSomething() (рисунок 2).

Рисунок 2. Отчет JUnit в разрезе классов
Отчет JUnit в разрезе классов

Если результат запуска теста отрицательный, то вы узнаете об этом как из вывода в командной строке, так и из отчета в HTML (рисунок 3).

Рисунок 3. Пример отрицательного завершения теста JUnit
Пример отрицательного завершения теста JUnit

Написание первого полезного теста

Теперь, запустив первый простейший пример, пора переходить к более реалистичному тесту. Допустим, что в классе HotelStay есть два поля: Date checkIn и Date checkOut. В соответствии с пользовательскими требованиями вывод метода toString должен выглядеть следующим образом: Hilton (Wednesday to Sunday). Представление дат в нужном формате не составляет труда благодаря классу java.text.SimpleDateFormat, корректность работы которого можно не проверять. Тест решает другие задачи: во-первых, он проверяет, что метод toString работает именно так, как задумано, а во-вторых, он дает гарантию того, что удовлетворены запросы пользователей.

Юнит-тесты – это "исполняемая" документация

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

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

Создание качественного, всеобъемлющего набора тестов позволяет не только избежать ошибок в коде. Дополнительным преимуществом является то, что тесты играют роль своего рода "исполняемой" документации, т.е. эволюционирующего представления функциональных требований в коде приложения. Если вам удастся провести параллели между тестами и пользовательскими требованиями, то вы сможете использовать тесты в общении с пользователями. У вас будет доказательство корректности кода и его соответствия требованиям. После этого создание исполняемой документации можно будет включить в процесс непрерывной интеграции, выполняемой при помощи, например, сервера CruiseControl (сервер, который постоянно выполняет наборы тестов), и в итоге вы получите механизм, который будет гарантировать, что реализация новых функций не вносит ошибок в существующее программное обеспечение.

Эта идея исполняемой документации полностью реализована в методологии разработки на основе функционирования (Behavior-Driven Development – BDD). Примером инфраструктуры BDD на Groovy является easyb, которая позволяет писать тесты в виде пользовательских требований, понятных как разработчикам, так и пользователям (см. раздел Ресурсы). Если вам посчастливилось работать с продвинутыми пользователями, которые не против отказаться от, например, Microsoft® Word, то вы сможете полностью отказаться от архаичных функциональных документов. В этом случае все требования к проекту могут быть исполняемыми.

Добавьте код, приведенный в листинге 5, в файлы HotelStay.groovy и HotelStayTests.groovy.

Листинг 5. Пример использования метода assertToString
import java.text.SimpleDateFormat
class HotelStay {
  String hotel
  Date checkIn
  Date checkOut
   
  String toString(){
    def sdf = new SimpleDateFormat("EEEE")
    "${hotel} (${sdf.format(checkIn)} to ${sdf.format(checkOut)})"
  }  
}


import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {

    void testSomething(){...}

    void testToString() {
      def h = new HotelStay(hotel:"Hilton")
      def df = new SimpleDateFormat("MM/dd/yyyy")
      h.checkIn = df.parse("10/1/2008")
      h.checkOut = df.parse("10/5/2008")
      println h
      assertToString h, "Hilton (Wednesday to Sunday)"
    }
}

Выполните команду grails test-app, чтобы убедиться, что второй тест завершается успешно.

Внутри testToString используется метод assertToString - один из двух новых контролирующих методов, предоставляемых классом GroovyTestCase. Разумеется, того же результата можно было бы добиться при помощи стандартного JUnit метода assertEquals, но вариант с assertToString несколько более выразителен. Имя и вызов данного метода не оставляют никаких сомнений насчет его предназначения. Полный список контролирующих методов, поддерживаемых классом GroovyTestCase, в том числе assertArrayEquals, assertContains и assertLength, можно найти по ссылке, приведенной в разделе Ресурсы.


Добавление контроллера и представлений

До этого момента все взаимодействие с классом HotelStay осуществлялось программным способом. Добавьте класс HotelStayController, показанный в листинге 6, чтобы можно было экспериментировать с классом HotelStay при помощи браузера.

Листинг 6. Класс HotelStayController
class HotelStayController {
  def scaffold = HotelStay
}

Далее необходимо слегка подкорректировать форму создания резервации. По умолчанию представление дат включает поля "день", "месяц", "год", "час" и "минуты" (рисунок 4).

Рисунок 4. Отображение даты и времени в форме резервации по умолчанию
Отображение даты и времени в форме резервации по умолчанию

В данном случае часть даты для представления времени можно спокойно отбросить. Выполните команду grails generate-views HotelStay. Для того чтобы обновить пользовательский интерфейс (как показано на рисунке 5), добавьте атрибут precision="day" в элементы <g:datePicker> на страницах views/hotelStay/create.gsp and views/hotelStay/edit.gsp.

Рисунок 5. Отображение только дат
Отображение только дат

Теперь, когда класс HotelStay функционирует внутри контейнера сервлетов, можно переходить к обсуждению различий между интеграционными и юнит-тестами.


Интеграционные и юнит-тесты

Как упоминалось выше, Grails поддерживает два основных типа тестов: юнит-тесты и интеграционные тесты. Между ними нет синтаксических различий – и те, и другие пишутся при помощи класса GroovyTestCase с использованием одних и тех же контролирующих методов. Все различия заключаются в семантике. Юнит-тесты тестируют классы поодиночке, в то время как интеграционные тесты позволяют проверять работу классов в рабочем окружении.

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

Юнит-тесты полезны в случае, если у вас есть Grails-классы, не относящиеся к ядру приложения. Для создания юнит-теста выполните команду grails create-unit-test MyTestUnit. Тестовые скрипты создаются в одном пакете, поэтому необходимо присваивать разные имена интеграционным и юнит-тестам. В противном случае будет выдан поток сообщений об ошибках, аналогичный показанному в листинге 7.

Листинг 7. Сообщение об ошибке в случае конфликта имен между интеграционным и юнит-тестом
The sources 
/src/trip-planner2/test/integration/HotelStayTests.groovy and 
   /src/trip-planner2/test/unit/HotelStayTests.groovy are 
   containing both a class of the name HotelStayTests.
 @ line 3, column 1.
   class HotelStayTests extends GroovyTestCase {
   ^

1 error

По умолчанию имена интеграционных тестов включают окончание Tests, поэтому для юнит-тестов мы будем использовать окончание UnitTests чтобы избежать конфликта имен.


Создание тестов для проверки сообщений о простых ошибках валидации

Следующим пользовательским требованием является то, что поле hotel должно обязательно быть заполнено. Это несложно гарантировать благодаря системе валидации, встроенной в Grails. Добавьте блок static constraints в класс HotelStay, как показано в листинге 8.

Листинг 8. Добавление блока static constraints в класс HotelStay
class HotelStay {
  static constraints = {
    hotel(blank:false)
    checkIn()
    checkOut()
  }
  
  String hotel
  Date checkIn
  Date checkOut
  
  //остальная часть класса остается без изменений
}

Далее выполните команду grails run-app. Теперь в ответ на попытку создания экземпляра HotelStay с пустым полем hotel вы должны получить сообщение об ошибке, показанное на рисунке 6.

Рисунок 6. Сообщение об ошибке по умолчанию, выдаваемое в ответ на незаполненное поле класса
Сообщение об ошибке по умолчанию, выдаваемое в ответ на незаполненное поле класса

Несомненно, пользователи будут благодарны за проверку, но вряд ли их устроит текст сообщения по умолчанию. Представьте, что требование к приложению выглядит так: "поле hotel является обязательным, а если оно не заполнено, то должно быть выдано сообщение "Please provide a hotel name" (пожалуйста, введите название отеля)".

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

Откройте файл grails-app/i18n/messages.properties и добавьте строку hotelStay.hotel.blank=Please provide a hotel name. Теперь попробуйте оставить поле hotel незаполненным в браузере. В ответ на это должно быть выдано только что добавленное сообщение (рисунок 7).

Рисунок 7. Вывод специального сообщения об ошибке валидации
Вывод специального сообщения об ошибке валидации

Добавьте новый тест в HotelStayTests.groovy, чтобы убедиться в правильности проверки заполнения названия отеля, как показано в листинге 9.

Листинг 9. Проверка наличия ошибок валидации
class HotelStayTests extends GroovyTestCase {
  void testBlankHotel(){
    def h = new HotelStay(hotel:"")
    assertFalse "there should be errors", h.validate()
    assertTrue "another way to check for errors after you call validate()", h.hasErrors()
  }

  //остальная часть класса остается без изменений
}

Как вы помните, каждый автоматически созданный класс модели включает метод save(). В этом тесте также можно было бы вызывать save(), однако в данном случае необязательно сохранять новый экземпляр в базе данных. Все что требуется - это проверить корректность валидации, для чего достаточно вызвать метод validate(). В случае ошибок валидации этот метод вернет false, а в противном случае - true.

Еще одним важным, с точки зрения валидации, методом является hasErrors(). Вызов этого метода после save() или validate() позволяет проверить, произошли ли ошибки.

В листинге 10 показан расширенный вариант теста testBlankHotel(), в котором иллюстрируется использование дополнительных методов валидации.

Листинг 10. Развернутый вариант теста на предмет наличия ошибок валидации
class HotelStayTests extends GroovyTestCase {
  void testBlankHotel(){
   def h = new HotelStay(hotel:"")
   assertFalse "there should be errors", h.validate()
   assertTrue "another way to check for errors after you call validate()", h.hasErrors()  
  
   println "\nErrors:"
   println h.errors ?: "no errors found"   
     
   def badField = h.errors.getFieldError('hotel') 
   println "\nBadField:"
   println badField ?: "hotel wasn't a bad field"
   assertNotNull "I'm expecting to find an error on the hotel field", badField


   def code = badField?.codes.find {it == 'hotelStay.hotel.blank'} 
   println "\nCode:"
   println code ?: "the blank hotel code wasn't found"
   assertNotNull "the blank hotel field should be the culprit", code
  }
}

Убедившись, что экземпляр вашего класса не проходит валидацию, можно вызвать метод getErrors() (в данном примере просто errors благодаря краткому синтаксису Groovy), который вернет экземпляр org.springframework.validation.BeanPropertyBindingResult. Подсистема валидации Grails внутри себя использует средства Spring аналогично тому, как GORM является легкой Groovy-оберткой вокруг Hibernate.

Результаты вызовов println не показываются в командной строке, но присутствуют в HTML-отчете (рисунок 8).

Рисунок 8. Просмотр информации, выведенной при помощи метода println внутри теста
Просмотр информации, выведенной при помощи метода println внутри теста

Нажмите на ссылку System.out в нижнем правом углу отчета HotelStayTests.

В листинге 10 используется так называемый оператор Элвиса (если повернуть его на 90 градусов, то он напоминает прическу и глаза знаменитого певца), который представляет собой краткую форму записи тернарного оператора в Groovy. Если ссылка слева от ?: равна null, то значением выражения является значение справа.

Измените название отеля на ""Holiday Inn"" и перезапустите тесты. Значением оператора Элвиса должно стать значение, показанное на рисунке 9.

Рисунок 9. Результаты работы оператора Элвиса в отчете
Результаты работы оператора Элвиса в отчете

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

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

Обратите внимание, что в данном тесте не проверяется текст сообщения об ошибке. Чем же он отличается от теста выше, в котором проверялось строковое значение, выдаваемое методом toString? Дело в том, что первом тесте метод toString занимал центральное место, в то время как сейчас важнее проверить корректность кода валидации, чем текст сообщения. Это подтверждение того, что тестирование – это больше искусство, чем наука. Если бы я хотел проверять конкретный текст сообщения, то имело бы смысл использовать Web-средства для тестирования, такие как Canoo WebTest или ThoughtWorks Selenium.


Написание и тестирование собственного кода валидации

Далее обратимся к следующему функциональному требованию. Приложение должно гарантировать, что значение поля checkOut всегда позже, чем checkIn (т.е. выселение из отеля происходит после въезда). Для этого требуется отдельный код валидации, который необходимо протестировать.

Добавьте код валидации, приведенный в листинге 11, в блок static constraints.

Листинг 11. Пример специализированного кода валидации
class HotelStay {
  static constraints = {
    hotel(blank:false)
    checkIn()
    checkOut(validator:{val, obj->
      return val.after(obj.checkIn)
    })
  }
  
  //остальная часть класса остается без изменений
}

В переменной val хранится текущее значение поля, а в переменной obj – текущий экземпляр класса HotelStay. В Groovy все объекты типа Date имеют методы before() и after(), поэтому для проверки дат достаточно просто вызвать метод after(). Если дата checkOut оказывается позже checkIn, то валидация проходит успешно. В противном случае возвращается false, что является признаком ошибки.

Далее выполните команду grails run-app. Убедитесь, что невозможно создать экземпляр HotelStay с датой checkOut ранее checkIn (рисунок 10).

Рисунок 10. Сообщение по умолчанию об ошибке валидации
Сообщение по умолчанию об ошибке валидации

Откройте файл grails-app/i18n/messages.properties и добавьте в него сообщение, которое будет выдаваться при ошибке валидации поля checkOut: hotelStay.checkOut.validator.invalid=Sorry, you cannot check out before you check in.

Сохраните файл messages.properties и вновь попробуйте создать некорректный экземпляр HotelStay. Вы должны увидеть сообщение об ошибке, как на рисунке 11.

Рисунок 11. Специализированное сообщение об ошибке валидации
Специализированное сообщение об ошибке валидации

Теперь пришло время написать тест (листинг 12).

Листинг 12. Пример тестирования специализированного кода валидации
import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {
  void testCheckOutIsNotBeforeCheckIn(){
    def h = new HotelStay(hotel:"Radisson")
    def df = new SimpleDateFormat("MM/dd/yyyy")
    h.checkIn = df.parse("10/15/2008")
    h.checkOut = df.parse("10/10/2008")
  
    assertFalse "there should be errors", h.validate()
    def badField = h.errors.getFieldError('checkOut') 
    assertNotNull "I'm expecting to find an error on the checkOut field", badField
    def code = badField?.codes.find {it == 'hotelStay.checkOut.validator.invalid'} 
    assertNotNull "the checkOut field should be the culprit", code                 
  }
}

Тестирование специализированных библиотек тегов

Осталось удовлетворить последнее требование пользователей. Нам удалось отбросить временную составляющую полей checkIn и checkOut в представлениях create и edit, но она все же присутствует в представлениях list и show, как можно убедиться из рисунка 12.

Рисунок 12. Отображение дат, включающее временную составляющую, принятое по умолчанию в Grails
Отображение дат, включающее временную составляющую, принятое по умолчанию в Grails

Легче всего эту проблему можно решить, создав собственную библиотеку тегов (TagLib). В качестве альтернативного решения можно воспользоваться тегом <g:formatDate>, но в написании собственного тега также нет ничего сложного. Нашей целью будет создание тега <g:customDateFormat>, который можно будет использовать двумя способами.

Во-первых, тег <g:customDateFormat> может служить в качестве обертки вокруг экземпляра Date и принимать на вход атрибут, значением которого может быть любая строка форматирования в соответствии с правилами SimpleDateFormat.

<g:customDateFormat format="EEEE">${new Date()}</g:customDateFormat>

Чаще всего будет требоваться выводить даты в соответствии с американским форматом "MM/dd/yyyy", поэтому имеет смысл сделать его форматом по умолчанию, который будет использоваться в случае, если не задан никакой другой.

<g:customDateFormat>${new Date()}</g:customDateFormat>

Далее, разобравшись с функциональными требованиями, выполните команду grails create-tag-lib Date (листинг 13), чтобы создать новый файл DateTagLib.groovy и соответствующий ему тестовый файл DateTagLibTests.groovy.

Листинг 13. Создание новой библиотеки тегов
$ grails create-tag-lib Date
[copy] Copying 1 file to /src/trip-planner2/grails-app/taglib
Created TagLib for Date
[copy] Copying 1 file to /src/trip-planner2/test/integration
Created TagLibTests for Date

Добавьте содержимое листинга 14 в файл DateTagLib.groovy.

Листинг 14. Создание собственного тега
import java.text.SimpleDateFormat

class DateTagLib {
  def customDateFormat = {attrs, body ->
    def b = attrs.body ?: body()
    def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
    
    //если не задан атрибут format, то использовать следующий формат:
    def pattern = attrs["format"] ?: "MM/dd/yyyy"
    out << new SimpleDateFormat(pattern).format(d)
  }
}

Класс тега принимает на вход строковые значения атрибутов, а также тело тега, после чего записывает строку в выходной поток. Поскольку данный тег будет являться оберткой вокруг неформатированного экземпляра Date, то понадобятся два объекта SimpleDateFormat. Один объект нужен для чтения строкового представления даты в виде, соответствующем формату по умолчанию метода Date.toString(). После того как строка разобрана и создан экземпляр Date, вызывается второй объект SimpleDateFormat для записи даты в другом формате в выходной поток.

Оберните поля checkIn и checkOut на странице list.gsp в новый тег, как показано в листинге 15.

Листинг 15. Использование специализированного тега
<g:customDateFormat>${fieldValue(bean:hotelStay, field:'checkIn')}</g:customDateFormat>

Выполните команду grails run-app и откройте в браузере страницу http://localhost:9090/trip/hotelStay/list, чтобы увидеть эффект от работы нового тега (рисунок 13).

Рисунок 13. Вывод дат при использовании нового тега
Вывод дат при использовании нового тега

Далее напишем пару тестов, чтобы убедиться в корректности работы тега (листинг 16).

Листинг 16. Тестирование специализированного тега
import java.text.SimpleDateFormat

class DateTagLibTests extends GroovyTestCase {
    void testNoFormat() {
      def output = 
         new DateTagLib().customDateFormat(format:null, body:"2008-10-01 00:00:00.0")
      println "\ncustomDateFormat using the default format:"
      println output
      
      assertEquals "was the default format used?", "10/01/2008", output
    }

    void testCustomFormat() {
      def output = 
         new DateTagLib().customDateFormat(format:"EEEE", body:"2008-10-01 00:00:00.0")
      assertEquals "was the custom format used?", "Wednesday", output
    }
}

Заключение

Теперь, когда вы написали несколько тестов и увидели, насколько просто тестировать работу компонентов Grails, вы можете продолжать следовать принципам разработки через тестирование. Таким образом, вы приобретете больше уверенности в результатах вашей работы. Более того, если вам удастся провести параллели между тестами и пользовательскими требованиями, то в награду вы получите подробную "исполняемую" и никогда не устаревающую документацию.

Темой следующей статьи будет объектная нотация JavaScript (JavaScript Object Notation – JSON). Grails предоставляет встроенную поддержку этого формата. Прочитав следующую статью, вы узнаете, как генерировать фрагменты JSON в контроллере и получать их со страниц GSP. Пока же получайте удовольствие от изучения Grails.

Ресурсы

Научиться

Получить продукты и технологии

  • Загрузите последний релиз Grails. (EN)
  • Загрузите easyb - инфраструктуру для разработки Java-приложений на основе функционирования (Behavior Driven Development). (EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=550780
ArticleTitle=Изучение Grails: Tестирование приложений Grails
publish-date=10132010