Как известно, в xUnit-фреймворках, простейший test-case состоит из последовательности вызовов SetUp, TestSomething, TearDown. И довольно часто в unit-тестировании требуется подготовить какие-то ресурсы перед основными тестами. Типичный пример этого — соединение с базой данных. И логика подсказывает нам, что было бы весьма затратно, запуская несколько тестов, перед каждым устанавливать соединение с БД в SetUp, и отключаться в TearDown.
... type TTestDB1 = class(TTestCase) protected public procedure SetUp; override; procedure TearDown; override; published procedure TestDB1_1; procedure TestDB1_2; end; ... implementation ... procedure TTestDB1.SetUp; begin inherited; // connect to DB end; procedure TTestDB1.TearDown; begin // disconnect from DB inherited; end; ... initialization RegisterTest(TTestDB1.Suite); end.
Схема вызовов будет такая:
-- TTestDB1.SetUp ---- TTestDB1.TestDB1_1 -- TTestDB1.TearDown -- TTestDB1.SetUp ---- TTestDB1.TestDB1_2 -- TTestDB1.TearDown
К тому же с БД может статься, что перед тем, как к БД подключиться, её нужно создать с требуемой структурой.
Для решения такой задачи в dUnit есть класс TTestSetup (описан в модуле TTestExtensions).
Он, по сути, реализует тот же интерфейс ITest
, что и TTestCase, то есть ту же схему: SetUp, Test…, TearDown, только вместо вызова тестов происходит вызов всего test-case’а, указанного при его создании. Т.е. видоизменив модуль:
uses ... TestExtensions; type TTestDBSetup = class(TTestSetup) public procedure SetUp; override; procedure TearDown; override; // published-методы в TTestSetup не запускаются end; TTestDB1 = ... ... implementation ... initialization RegisterTest(TTestDBSetup.Create(TTestDB1.Suite)); end.
получим схему вызовов:
-- TTestDBSetup.SetUp ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_1 ---- TTestDB1.TearDown ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_2 ---- TTestDB1.TearDown -- TTestDBSetup.TearDown
По сути, это схема suite + test-cases. Таким образом, устанавливая соединение к БД в TTestDBSetup.SetUp, мы сделаем это лишь однажды перед запуском TestDB1_1 и TestDB1_2.
Это доcтаточно понятно, когда у нас только один test-case с тестами, требующий соединения с БД. Но что делать, когда мы хотим создать второй test-case, которому также нужно соединение с БД (назовём его TTestDB2 с методами TestDB2_1, TestDB2_2, и т.д)?
Конструктор TTestSetup.Create
описан так:
constructor TTestSetup.Create(ATest: ITest; AName: string = '');
То есть «включать» в suite можно только лишь один test-case. Если мы напишем так:
RegisterTest(TTestDBSetup.Create(TTestDB1.Suite)); RegisterTest(TTestDBSetup.Create(TTestDB2.Suite));
То получим вызовы по схеме:
-- TTestDBSetup.SetUp ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_1 ---- TTestDB1.TearDown ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_2 ---- TTestDB1.TearDown -- TTestDBSetup.TearDown -- TTestDBSetup.SetUp ---- TTestDB2.SetUp ------ TTestDB2.TestDB2_1 ---- TTestDB2.TearDown ---- TTestDB2.SetUp ------ TTestDB2.TestDB2_2 ---- TTestDB2.TearDown -- TTestDBSetup.TearDown
Это не то, что мы хотим. Мы ведь хотим подключиться к БД лишь единожды.
Тут и начинается, собственно, то, что побудило меня написать эту статью. Обратим внимание на второй вариант метода RegisterTest:
procedure RegisterTest(SuitePath: string; test: ITest); begin assert(assigned(test)); if __TestRegistry = nil then CreateRegistry; RegisterTestInSuite(__TestRegistry, SuitePath, test); end;
Что за SuitePath
? Смотрим RegisterTestInSuite
:
procedure RegisterTestInSuite(rootSuite: ITestSuite; path: string; test: ITest); ... begin if (path = '') then begin // End any recursion rootSuite.addTest(test); end else begin // Split the path on the dot (.) dotPos := Pos('.', Path); if (dotPos <= 0) then dotPos := Pos('\', Path); if (dotPos <= 0) then dotPos := Pos('/', Path); if (dotPos > 0) then begin suiteName := Copy(path, 1, dotPos - 1); pathRemainder := Copy(path, dotPos + 1, length(path) - dotPos); end else begin suiteName := path; pathRemainder := ''; end; ...
И видим, что SuitePath разбивается на части, а разделитель этих частей — точка, т.е. это некий «путь suite», в который добавляется регистрируемый test-case.
Пробуем TestDB2 зарегистрировать так (чтобы добавить TTestDB2 «дочерним узлом» в TTestDBSetup):
RegisterTest('Setup decorator ((d) TTestDB1)', TTestDB2.Suite);
Не получилось:
Смотрим опять код RegisterTestInSuite
:
procedure RegisterTestInSuite(rootSuite: ITestSuite; path: string; test: ITest); ... begin ... currentTest.queryInterface(ITestSuite, suite); if Assigned(suite) then begin ...
Видим, что test-case добавляется в ITestSuite, а TTestSetup не реализует этот интерфейс. Как же быть?
Тут подглядываем, например, в библиотеку IndySoap (в ней есть тесты dUnit, организованные по группам) и видим там примерно следующее (запишем сразу применительно к нашим тестам):
... function DBSuite: ITestSuite; begin Result := TTestSuite.Create('DB tests'); Result.AddTest(TTestDB1.Suite); Result.AddTest(TTestDB2.Suite); end; ... initialization RegisterTest(TTestDBSetup.Create(DBSuite));
То есть мы создаём suite из наших test-case’ов, а уже этот suite добавляем в TTestSetup.
И вроде бы, всё работает, и всё хорошо. На этом можно бы и закончить.
Но если (точнее, «когда») мы будем добавлять ещё тесты БД (назовём их, TTestDB3), то нам придётся добавлять их и в DBSuite:
... function DBSuite: ITestSuite; begin ... Result.AddTest(TTestDB3.Suite); end; ...
Кроме того, по-хорошему, их надо выносить в отдельный модуль, а уже этот модуль добавлять в модуль с функцией DBSuite. Это изменение DBSuite лично мне не очень нравится (к тому же, визуально в иерархии тестов добавляется «лишний» узел «DB tests», хотя TTestDB1/TTestDB2 могли бы «принадлежать» сразу TTestDBSetup). Я хочу лишь добавить модуль тестов в проект и они «автоматически» добавились бы в TTestDBSetup.
Что ж, сделаем как хотим. Во-первых, мне не нравится имя Setup’а вида «Setup decorator ((d)…». К тому же, потом, когда мы будем регистрировать другие тесты в этот Setup, мы будем использовать это имя. Поменяем. Для этого обратим внимание на следующее:
function TTestSetup.GetName: string; begin Result := Format(sSetupDecorator, [inherited GetName]); end;
И на параметр AName
в
constructor TTestSetup.Create(ATest: ITest; AName: string = '');
Который в итоге присваивается
constructor TAbstractTest.Create(AName: string); ... FTestName := AName; ...
Так что, если мы переопределим
... TTestDBSetup = ... public function GetName: string; override; ... implementation ... function TTestDBSetup.GetName: string; begin Result := FTestName; end; ... initialization RegisterTest(TTestDBSetup.Create(DBSuite, 'DB'));
То получим:
Теперь хочется регистрировать test-case’ы сразу при подключении модуля в проект. То есть так:
unit uTestDB3; ... initialization RegisterTest('DB', TTestDB3.Suite));
Для этого надо (вспомним RegisterTestInSuite
), чтобы TTestDBSetup реализовывал интерфейс ITestSuite.
... ITestSuite = interface(ITest) ['{C20E38EF-7369-44D9-9D84-08E84EC1DCF0}'] procedure AddTest(test: ITest); procedure AddSuite(suite : ITestSuite); end;
Там всего-то два метода:
... TTestDBSetup = class(TTestSetup, ITestSuite) public procedure AddTest(test: ITest); procedure AddSuite(suite : ITestSuite); end; ... implementation ... procedure TTestDBSetup.AddTest(test: ITest); begin Assert(Assigned(test)); FTests.Add(test); end; procedure TTestDBSetup.AddSuite(suite: ITestSuite); begin AddTest(suite); end; ...
Получилось!
Однако, при запуске (F9, кстати) оказывается, что тесты TTestDB3 не выполняются:
Чтобы понять почему, посмотрим на реализацию:
procedure TTestDecorator.RunTest(ATestResult: TTestResult); begin FTest.RunWithFixture(ATestResult); end;
Т.е. тесты запускаются только те (FTest
), которые были заданы при создании TTestDBSetup:
constructor TTestDecorator.Create(ATest: ITest; AName: string); begin ... FTest := ATest; FTests:= TInterfaceList.Create; FTests.Add(FTest); end;
А которые мы добавили позже (FTests
) — нет. Запустим и их, переопределив RunTest:
... TTestDBSetup = ... protected procedure RunTest(ATestResult: TTestResult); override; ... end. ... procedure TTestDBSetup.RunTest(ATestResult: TTestResult); var i: Integer; begin inherited; // пропустим первый элемент, т.к. это FTest for i := 1 to FTests.Count - 1 do (FTests[i] as ITest).RunWithFixture(ATestResult); end;
Запускаем:
Вот теперь, вроде, всё ок. Однако, если приглядеться, то увидим, что в статистике количество тестов — 4, а было запущено — 6. Очевидно, наши добавленные тесты не учитываются. Непорядок.
Наведём красоту:
... TTestDBSetup = ... protected ... function CountTestInterfaces: Integer; function CountEnabledTestInterfaces: Integer; public ... function CountTestCases: Integer; override; function CountEnabledTestCases: Integer; override; end; ... function TTestDBSetup.CountTestCases: Integer; begin Result := inherited; if Enabled then Inc(Result, CountTestsInterfaces); end; function TTestDBSetup.CountTestInterfaces: Integer; var i: Integer; begin Result := 0; // skip FIRST test case (it is FTest) for i := 1 to FTests.Count - 1 do Inc(Result, (FTests[i] as ITest).CountTestCases); end; function TTestDBSetup.CountEnabledTestCases: Integer; begin Result := inherited; if Enabled then Inc(Result, CountEnabledTestInterfaces); end; function TTestDBSetup.CountEnabledTestInterfaces: Integer; var i: Integer; begin Result := 0; // skip FIRST test case (it is FTest) for i := 1 to FTests.Count - 1 do if (FTests[i] as ITest).Enabled then Inc(Result, (FTests[i] as ITest).CountTestCases); end; ...
Здесь CountEnabledTestCases и CountEnabledTestInterfaces — вспомогательные функции.
Nota bene. В GUI варианте учитывается CountEnabledTestCases, а в консольном — CountTestCases.
Вот теперь порядок.
Дочитавший до конца читатель может спросить, а стОит ли так заморачиваться вместо использования функции по типу вышеописанной DBSuite? Я и сам об этом сейчас подумал. Но для меня один из плюсов данного решения состоит в том, что переделка одного моего проекта, в котором я, ещё до того, как разобрался с dUnit настолько, делал немного по-другому. И для приведения к такой красивости там понадобится подправить лишь один пару методов (ну и добавить вышеописанное в базовый класс).
P.S.: Исходные коды примера — github.com/ashumkin/habr-dunit-ttestsetup-demo
ссылка на оригинал статьи http://habrahabr.ru/post/266487/
Добавить комментарий