Больше всего программисты любят программы, в которых не нужно исправлять баги. Шагом на пути к этой несбыточной мечте является написание юнит-тестов. В Cache, как и в любой современной СУБД, есть реализация фреймворка для автоматического выполнения тестов.



За подробным описанием философии и методологии написания юнит-тестов я вас отсылаю в интернет – на Хабр и к замечательной книге «Growing Object-Oriented Software Guided by Tests». На русский язык она не переведена, но английский там простой и понятный.

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

По всем канонам TDD сначала создадим тест:

Class Tutorial.Test.Person Extends %UnitTest.TestCase
{
Method TestNewPerson() {
    
set surname "John"
    
set name "Doe"
    
set job "quality assurance developer"
    
set id ##class(Tutorial.Person).AddPerson(surnamenamejob, .ec)
    
do $$$AssertStatusOK(ec)
    
    
set ##class(Tutorial.Person).%OpenId(id)
    
do $$$AssertTrue($IsObject(p))
    
    
do $$$AssertEquals(p.Surnamesurname)
    
do $$$AssertEquals(p.Namename)
    
do $$$AssertEquals(p.Jobjob)
}
}

Класс, содержащий тесты, должен наследовать от класса %UnitTest.TestCase. Все методы этого класса, которые начинаются со слова Test, считаются тестами. Макросы и методы, доступные наследникам %UnitTest.TestCase, описаны в документации.

Тесты запускает метод ##class(%UnitTest.Manager).RunTest(subdir, spec). Этот метод загружает и выполняет все классы с тестами из подпапки subdir каталога, который указан в глобале ^UnitTestRoot. Spec — дополнительные ключи для запуска. По умолчанию после выполнения классы с тестами удаляются. Мы этого, конечно, не допустим, добавив вторым аргументом к RunTest строку с ключом «/nodelete».

Итак, создайте папку c:\unittests\habr и экспортируйте в неё наш класс с тестом (понятно, что папку вы можете назвать как угодно и что подпапку лучше бы было назвать tutorial, обозначая пакет, который мы будет проверять; но, чтобы подчеркнуть, что эти названия не обязаны совпадать, я назвал подпапку habr). Экспортируйте тест в эту папку (я назвал файл tutorial.test.person.xml) и вперёд в терминал!

USER>set ^UnitTestRoot="c:\UnitTests"
 
USER>do ##class(%UnitTest.Manager).RunTest("habr","/nodelete/noload")
 
==================================================================
Directory: C:\UnitTests\habr==================================================================
  habr begins ...
Перечислить стартовавшие элементы в каталоге 07/26/2015 00:59:23 '*.xml;*.XML'
 
Вывести файл C:\UnitTests\habr\tutorial.test.person.xml в xml
Вывод завершен успешно.
 
    Tutorial.Test.Person begins ...
      TestNewPerson() begins ...
LogStateStatus:0:TestNewPerson:ОШИБКА #5002: Ошибка: <CLASS DOES NOT EXIST>zTestNewPerson+4^Tutorial.Test.Person.1 *Tutorial.Person  <<==== **FAILED**
habr:Tutorial.Test.Person:TestNewPerson:
      TestNewPerson failed
    Tutorial.Test.Person failed
  Skipping deleting classes
  habr failed
 
Use the following URL to view the result:
http://192.168.1.6:57772/csp/sys/%25UnitTest.Portal.Indices.cls?Index=2&$NAMESPACE=USER
Some tests FAILED in suites:
  habr

Обратите внимание — я указал два ключа в методе RunTest: /nodelete — не удалять класс с тестами после запуска, /noload — не загружать и не компилировать сам класс.

Как видите, наш тест благополучно завершился с ошибкой. В конце вывода напечатан URL для веб-интерфейса результатов запуска этого теста и истории запусков. Обратите внимание, что этот URL ссылается на веб-приложение /csp/sys, указывая нужную область аргументом. Если вы в Портале управления смените область на USER и потом перейдёте по ссылкам Обозреватель системы > Инструменты > Портал UnitTest, то, вероятно, вы увидите ошибку Forbidden при попытке открыть следующий URL:

http://<yourhost>:<yourport>/csp/user/%25UnitTest.Portal.Home.zen?$NAMESPACE=USER

Эта ошибка вызвана тем, что в Cache по умолчанию запрещено обращение к системным (процентным) страницам из веб-приложений не начинающихся на /csp/sys. Чтобы разрешить открытие страниц, относящихся к юнит-тестам в области USER, выполните следующую команду в терминале в области %SYS:

%SYS>Set ^SYS("Security","CSP","AllowPrefix","/csp/user/","%UnitTest.")=1

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

Class Tutorial.Test.All Abstract ]
{
ClassMethod runall()
{
    
do $system.OBJ.Export("Tutorial.Test.Person.cls", ^UnitTestRoot_"\habr\"_"tutorial.test.person.xml")
    
; здесь можно добавлять экспорт новых классов с тестами.
    
    
do ##class(%UnitTest.Manager).RunTest("habr","/nodelete/noload")
}
}

Последнее, что осталось, это создать класс Tutorial.Person и метод AddPerson, запустить тесты и увидеть долгожданное «ALL PASSED»:

USER>do ##class(Tutorial.Test.All).runall()
 
Экспорт  в XML начался в 07/26/2015 01:25:12
Экспортируемый класс: Tutorial.Test.Person
Экспорт успешно завершен.
 
==================================================================
Directory: C:\UnitTests\habr==================================================================
  habr begins ...
Перечислить стартовавшие элементы в каталоге 07/26/2015 01:25:12 '*.xml;*.XML'
 
Вывести файл C:\UnitTests\habr\tutorial.test.person.xml в xml
Вывод завершен успешно.
 
    Tutorial.Test.Person begins ...
      TestNewPerson() begins ...
        AssertStatusOK:ec (passed)
        AssertTrue:$IsObject(p) (passed)
        AssertEquals:p.Surname== surname (passed)
        AssertEquals:p.Name== name (passed)
        AssertEquals:p.Job== job (passed)
        LogMessage:Duration of execution: .000503 sec.
      TestNewPerson passed
    Tutorial.Test.Person passed
  Skipping deleting classes
  habr passed
 
Use the following URL to view the result:
http://192.168.1.6:57772/csp/sys/%25UnitTest.Portal.Indices.cls?Index=7&$NAMESPACE=USER
All PASSED

На этом месте пытливый читатель (привет, tsafin) возмутится: «Зачем столько извращений, чтобы запустить простой тест?! Нельзя ли как-нибудь попроще?». Конечно нельзя можно! Но не сильно проще. Если папка c:\UnitTests\habr есть и в ^UnitTestRoot прописан путь «c:\UnitTests», то отдельно взятый класс с тестами можно (без предварительной выгрузки) запустить так:

do ##class(%UnitTest.Manager).DebugRunTestCase("habr","Tutorial.Test.Person")

Не забывайте, Cache — это СУБД. Все результаты тестов сохраняются и доступны для SQL-запросов. Откройте окно Портала для ввода SQL. Поставьте галочку рядом со словом Система, чтобы показать системные классы. Выберите в выпадающем списке схему %UnitTest_Result. Например, в столбце Duration некоторых таблиц этой схемы хранится продолжительность отдельного теста (или метода внутри теста). Запуская тесты регулярно, вы можете следить не только за тем, чтобы они не падали, но и чтобы время их выполнения не увеличивалось.

В заключение рекомендую пройти небольшой туториал по юнит-тестам, который есть в документации по Cache, а также просмотреть описание классов пакета %UnitTest в справочнике классов.

Комментарии (9)


  1. J_K
    29.07.2015 00:31
    +2

    >>Больше всего программисты любят программы, в которых не нужно исправлять баги. Шагом на пути к этой несбыточной мечте является написание юнит-тестов.

    Уж поверьте, исправлять баги гораздо интереснее и веселее, чем писать юнит-тесты :))


    1. adaptun Автор
      29.07.2015 00:39
      +3

      Везёт вам.


      1. J_K
        29.07.2015 01:09
        +1

        Я не говорю, что иметь баги правильнее, чем юнит-тесты, но лично для меня написание юнит-тестов — это самая худшая часть программирования :-0


        1. adaptun Автор
          29.07.2015 13:41
          +1

          Если вас это устраивает, то ok.
          А если нет — то ещё рекламирую отличную книжку «Growing object-oriented software guided by tests», в которой отлично описана философия юнит-тестов и TDD.


    1. doublefint
      29.07.2015 12:22
      +5

      Весело исправлять баги без тестов?

      Как-то так?


  1. olgafm
    29.07.2015 02:06
    +1

    Добрый день! Очень подробно, хорошо написано. Мне кажется было бы плюсом описать способ автоматического запуска с помощью Jenkins, например. Не будут же разработчики, особенно если команда большая, запускать тесты каждый раз после коммита из терминала каше. Как считаете?


    1. morisson
      29.07.2015 10:42
      +1

      Согласен. И вот тут есть рецепт doublefint, как приготовить Jenkins с Cache


    1. eduard93
      29.07.2015 12:28
      +2

      Можно использовать хуки в Continuous Integration проекте CacheGitHubCI для запуска юнит тестов. Преимуществом проекта является также то, что он полностью написан на COS. После каждого коммита тесты будут запущены автоматически.


    1. adaptun Автор
      29.07.2015 12:34
      +1

      Можно включить аутентификацию на уровне ОС (http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS_secmgmt#GCAS_secmgmt_autheopts) и запускать программы из командной строки Windows:
      ccontrol cterminal CACHE ^^test
      (http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GSA_using_instance#GSA_B150513)

      В программе ^test может быть всё что угодно, в том числе запуск тестов