За подробным описанием философии и методологии написания юнит-тестов я вас отсылаю в интернет – на Хабр и к замечательной книге «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(surname, name, job, .ec)
do $$$AssertStatusOK(ec)
set p = ##class(Tutorial.Person).%OpenId(id)
do $$$AssertTrue($IsObject(p))
do $$$AssertEquals(p.Surname, surname)
do $$$AssertEquals(p.Name, name)
do $$$AssertEquals(p.Job, job)
}
}
Класс, содержащий тесты, должен наследовать от класса %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) возмутится: «Зачем столько извращений, чтобы запустить простой тест?! Нельзя ли как-нибудь попроще?». Конечно
do ##class(%UnitTest.Manager).DebugRunTestCase("habr","Tutorial.Test.Person")
Не забывайте, Cache — это СУБД. Все результаты тестов сохраняются и доступны для SQL-запросов. Откройте окно Портала для ввода SQL. Поставьте галочку рядом со словом Система, чтобы показать системные классы. Выберите в выпадающем списке схему %UnitTest_Result. Например, в столбце Duration некоторых таблиц этой схемы хранится продолжительность отдельного теста (или метода внутри теста). Запуская тесты регулярно, вы можете следить не только за тем, чтобы они не падали, но и чтобы время их выполнения не увеличивалось.
В заключение рекомендую пройти небольшой туториал по юнит-тестам, который есть в документации по Cache, а также просмотреть описание классов пакета %UnitTest в справочнике классов.
Комментарии (9)
olgafm
29.07.2015 02:06+1Добрый день! Очень подробно, хорошо написано. Мне кажется было бы плюсом описать способ автоматического запуска с помощью Jenkins, например. Не будут же разработчики, особенно если команда большая, запускать тесты каждый раз после коммита из терминала каше. Как считаете?
morisson
29.07.2015 10:42+1Согласен. И вот тут есть рецепт doublefint, как приготовить Jenkins с Cache
eduard93
29.07.2015 12:28+2Можно использовать хуки в Continuous Integration проекте CacheGitHubCI для запуска юнит тестов. Преимуществом проекта является также то, что он полностью написан на COS. После каждого коммита тесты будут запущены автоматически.
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 может быть всё что угодно, в том числе запуск тестов
J_K
>>Больше всего программисты любят программы, в которых не нужно исправлять баги. Шагом на пути к этой несбыточной мечте является написание юнит-тестов.
Уж поверьте, исправлять баги гораздо интереснее и веселее, чем писать юнит-тесты :))
adaptun Автор
Везёт вам.
J_K
Я не говорю, что иметь баги правильнее, чем юнит-тесты, но лично для меня написание юнит-тестов — это самая худшая часть программирования :-0
adaptun Автор
Если вас это устраивает, то ok.
А если нет — то ещё рекламирую отличную книжку «Growing object-oriented software guided by tests», в которой отлично описана философия юнит-тестов и TDD.
doublefint
Весело исправлять баги без тестов?
Как-то так?