Форсоване тестування процедур T-SQL, Інші СУБД, Бази даних, статті

Використання динамічних запитів і web-звітів для автоматизації тестів

Раніше тестування процедур, як правило, було заключній «санітарної» перевіркою перед випуском нової процедури: чи чітко працює процедура і чи виконує всі покладені на неї завдання? Якщо відповідь позитивний, то можна випускати процедуру і рухатися далі. Тільки у деяких адміністраторів баз даних знаходилося час зайнятися нетиповими сценаріями використання, такими як неправильні значення параметрів, відсутність даних або недоступні об’єкти, наприклад робочі таблиці. Але цей лінійний підхід більше не працює. Тепер відомо, що збережені процедури, як і інше програмне забезпечення, перед випуском необхідно тестувати і перевіряти ще раз до тих пір, поки вони не будуть відповідати встановленим стандартам. І тут нас очікує перешкода, тому що тестування модуля – процес непростий. Воно вимагає трудомісткого введення тестових значень, відстеження результатів тесту в спробі визначити, чи виконує процедура все те, що передбачалося – і нічого більше.


Коли завдання не так проста, програміст, природно, починає шукати кошти автоматизувати рутинну роботу. Хіба не було б чудово скоротити час на тестування модуля збережених процедур T-SQL? Тепер це можливо. Ми можемо автоматизувати тестування процедури шляхом використання тільки Query Analyzer і невеликого коду на T-SQL. Таке рішення має чотири легко реалізуються компонента, і цей список можна по необхідності доповнити: таблиця, що зберігає тестові варіанти і ідентифікує процедури, які ви збираєтеся виконувати; динамічно генерований код на T-SQL, що виконує тестові приклади відповідно до процедур; таблиця, що зберігає результати тестів і системну збережену процедуру сервера SQL Server sp_makewebtask для створення звіту за результатами тестів.


Перевірка модулів і автоматизація тестування


Для початку зробимо побіжний огляд того, що включає в себе тестування модуля збереженої процедури. У простому випадку тестові приклади прописуються з метою пошуку помилок. Для процедури “виконання коду “просто означає перевірку виклику процедури на коректність імені. Для процедур, що містять вхідні параметри, це означає включення значень параметрів нарівні з ім’ям процедури; для процедур, що містять вихідні параметри – оголошення локальної змінної для отримання вихідних значень. З точки зору базових механізмів тут немає нічого складного.


Іноді справа ускладнюється вибором вірних тестових значень і визначенням системи взаємодії цих значень зі станом бази даних, в той час як ви виконуєте свої тести. Припустимо, що ви вже знаєте, які тестові значення хочете використовувати, не риючись в стратегіях вибору значень, таких як рівнозначне розподіл (наприклад, вибір вхідних значень, які здійснюють дійсні та недійсні стану ресурсу, такі як вставка в існуючу таблицю – дійсне – і, навпаки, спроба вставки в неіснуючу таблицю – недійсне) і полузначімий аналіз (наприклад, вибір вхідних значень, досліджують крайні умови, такі як місяці 1 і 12 при перевірці дат). Під станом бази даних маються на увазі її попередні умови (тобто вимоги або залежності), такі як необхідні процедури об’єкти і відомості про те, чи має користувач доступ до цих об’єктів.


Коли відомо стан бази даних, можна визначити настановні і виконувані механізми, які необхідно відтворити для автоматизації процедури. Виконання тесту по відношенню до збережених процедур вимагає знання кількох значень: значень будь-яких вхідних параметрів, переданих в процедуру, що повертає значення процедури (щоб допомогти визначити, допущені чи помилки), будь-які значення параметрів, які очікуються на виході, і будь-які значення, які процедура виводить на екран або відправляє в таблицю. В процесі виконання тесту ви визначаєте результат (пройшов – не пройшов) шляхом порівняння очікуваних вихідних даних з отриманими і перевірки поворотного значення процедури на невірні умови (у формі помилкових користувальницьких кодів), такі як відсутня таблиця або неправильні дозволу.


Отже, основи тестових механізмів достатньо прості. Користуючись процедурними тестами, можна також виконати перевірку стану об’єктів бази даних до і після тестування (наприклад, занесення даних в таблицю), але ці види тестів тут розглядатися не будуть.


Запис тестових варіантів


Як відбувається автоматизація виконання тестових варіантів? На практиці тестовий варіант включає в себе назву процедури та будь-які асоціативні вхідні значення, параметри виходу або повертаються значення. Почнемо зі створення таблиці для зберігання імен та параметрів деяких показових процедур для подальшого тестування, що показано в Лістингу 1. Щоб не ускладнювати код, в одну й ту ж таблицю тестових значень tabTestValues ​​включені і імена процедур, і дані параметра. В цьому випадку всі тестові дані розташовані централізовано, так, що можна легко додати нові тестові варіанти і запросити таблицю звітів про підтвердження якості. Якщо необхідна нормалізація, можна створити окремі таблиці для процедур і даних параметра. Таблиця 1 ілюструє опис кожного стовпця таблиці тестових значень tabTestValues. При бажанні можна легко розширити tabTestValues ​​для включення додаткових відомостей, таких як автор тестового варіанту; зазначення дати і часу; номер версії процедури (з метою контролю збереження та управління створенням); власник процедури (для повідомлення, особливо в разі невдачі тесту); та вимоги до використання або особливості впровадження процедури (для забезпечення можливості відновлення).


Тепер, коли з’явилося місце для зберігання тестів, заповнимо таблицю tabTestValues ​​кількома зразками тестових варіантів. Перші три команди INSERT в Лістингу 2 створюють тестовий випадок для збереженої процедури під назвою Sproc_1, яка займає три рядки в таблиці tabTestValues ​​- один рядок для кожного з його вхідних параметрів. В Лістингу 3 показаний процес створення коду для збережених процедур, які посилаються на таблицю tabTestValues. Sproc_1 не має параметрів виходу і лише виводить свої вхідні значення.


Наступні чотири команди INSERT в Лістингу 2 створюють тестовий випадок для Sproc_2, процедури, яка містить три вхідних параметра і вихідний параметр – сукупна вартість – це тип даних «гроші». Структура Sproc_2 складніша. При отриманні даних про назву товару, кількості і ціною Sproc_2 підраховує загальну вартість замовлення (quantity * price – кількість * ціна) і потім використовує параметр виходу @ total, щоб перейти до процесу виклику (в даному випадку Query Analyzer). Дві наступні команди INSERT в Лістингу 2 створюють тестовий випадок для Sproc_3. Ця процедура вимагає last name, потім використовує вихідний параметр для повернення повідомлення про помилку типу varchar (30). Ця логічна схема показує, як автоматизований тест-драйвер може управляти різними типами даних. Нарешті, процедура up_lookupPrice запитує таблицю під назвою Products – Товари (яка була навмисно пропущена в цьому прикладі, щоб викликати помилку “missing resourse”) – про ціну, потім повертає цю ціну в зухвалу программу.Up_lookupPrice являє додаткову реалізацію шляхом додаткових перевірок на помилки в реальному часі і повернення кодів, що використовуються зазвичай на рівні створення збережених процедур. Ця процедура також впливає на тест-драйвер для взаємодії із загальним порушенням залежності в проходженні таблиці Products.


Автоматичне тестування з Up_autoTest


Тепер, коли таблиця Тестових Значень – tabTestValues ​​- завантажена тестовими варіантами, розглянемо уважніше тест-драйвер, збережену процедуру up_АutoTest, що показано в Лістингу 4. up_AutoTest відображає поточний стан тестових оточень. Ця процедура, що зберігається має безліч переваг. Вона самодостатня (в даному випадку виконується безпосередньо з Query Analyzer), тестує будь-яку кількість процедур за один прийом і виконує будь-яку кількість тестів на одній і тій же процедурі. Крім того, дана процедура, що зберігається працює з усіма процедурами загального користування (процедурами без певних параметрів, процедурами тільки з вхідними параметрами або вихідними параметрами), підтримує будь-яку кількість вхідних параметрів (аж до дозволеного межі T-SQL), підтримує всі стандартні типи даних T-SQL (наприклад, int, money, char). Up_autoTest забезпечує стандартний формат для всіх тестів, отримує повертаються значення, як і вихідні значення, коли вони доступні; дозволяє тестерам і розробникам процедур по необхідності працювати незалежно один від одного. Вона проста в застосуванні, підтримки та модифікації.


Щоб зрозуміти, як працює Up-autoTest, розглянемо механізми використання Query Analyzer для перевірки вручну збереженої процедури Up_lookupPrice, що показано в лістингу 3. Для початку варто оголосити локальну змінну – @ price – щоб отримати вихідну значення процедури up_lookupPrice. Потім, для перевірки умов можливої ​​помилки, слід оголосити ще одну локальну змінну – @ return value – для отримання значення, що повертається процедури up_lookupPrice. Далі слід вибрати тестове значення – таке як 4 – для вхідного параметра @ product_id процедури up_lookupPrice, @ product_id. І, нарешті, необхідно включити команду EXEC, за якою йдуть запити SELECT, що демонструють значення @ price і @ return_value. Наведений нижче код показує процес такого тестування:






DECLARE @price money, @return_value int 
EXEC @return_value = up_lookupPrice 4, @price OUTPUT
SELECT @price “Unit price”
SELECT @return_value “Return Value”

Оскільки необхідної таблиці Products не існує, виконання процедури up_lookupPrice призводить до значення null Unit Price і повертається значенням 10 (користувальницький код, який означає відсутність таблиці в Лістингу 3). Через цю помилку отримуємо висновок про те, що процедура up_lookupPrice даний тест не пройшла.


Виникає питання, як автоматизувати кроки, виконувані вручну для перевірки процедури up_lookupPrice. Таблиця TabTestValues ​​вже містить всю необхідну інформацію: назву процедури, імена та тестові значення всіх вхідних параметрів, імена та очікувані значення для всіх вихідних параметрів. Тому для автоматизації всіх етапів тестування потрібно просто написати код на T-SQL, який об’єднає всі ці дані в тестові сценарії, такі як той, що вже складено вручну, а потім виконає ці сценарії. Далі код на T-SQL порівняє очікувані та одержані результати, перехопить будь-який вказаний код помилки, визначить результати тесту, запише їх для подальшого використання і випустить Web-звіт, демонструє результати тесту.


Створення коду на T-SQL – головне в цьому процесі, але тільки здатність виконувати збережені процедури (або інший код на T-SQL) з ходу робить можливим автоматизоване тестування. Затримуючи конструкцію запиту до часу виконання, творці тесту можуть зберігати імена процедур і деталі тесту в таблиці метаданих (такий як таблиця tabTestValues), не турбуючись про те, коли, ким або як часто ці тести в кінцевому рахунку будуть виконуватися.


Для наочності розглянемо на прикладі лістингу 4, як процедура, що зберігається up_AutoTest динамічно тестує up_lookupPrice. Спочатку процедура up_AutoTest ідентифікує up_lookupPrice як тестовану процедуру шляхом вибору її proc_name та асоціативних параметрів, внесених в таблицю tabTestValues. Далі процедура up_AutoTest використовує курсор для динамічного побудови тестового сценарію, який зв’язує потрібний синтаксис виконання (ім’я процедури поряд з будь-якими параметрами і шаблонними змінними, необхідними для отримання повертаються значень або вихідних даних параметрів) з локальної змінної, @ query. Як тільки скрипт готовий, процедура up_AutoTest виконує @ query.


Як передбачалося, процедура up_AutoTest виконує будь-яку кількість послідовних одиничних тестів. Якщо необхідно виконати окремі процедури, можна переробити up_AutoTest, включивши один вхідний параметр з посиланням на ім’я процедури. Схема 1 демонструє послідовність етапів процедури up_AutoTest.


Звіт за результатами тестів


Згідно з вимогами, результати тестів повинні бути оформлені в звіт. Для створення звіту знадобиться ще одна таблиця, tabTestResults, яка створюється кодом в Лістингу 5. Тепер, після виконання всіх тестів, збережена процедура up_AutoTest запитує в таблиці tabTestResults повертаються значення, вихідні параметри і очікувані тестові результати. Потім дана процедура оцінює ці три види значень і визначає результат тесту. Можливі й інші шляхи визначення тестових результатів. На закінчення, ця процедура встановлює графу test_result на Pass або Fail (Виконано – Не виконано) і модернізує tabTestResults для включення результатів тесту.


Щоб робота з тестування була доступна іншим користувачам, процедура up_AutoTest також включає автоматичний web-звіт. Слід тільки визначити діючий маршрут файлу в заключному блоці коду лістингу 4, і процедура up_AutoTest запросить в таблиці tabTestResults необхідні дані, а потім виконає системну процедуру sp_makewebtask, виконавчий механізм Web Assistant в SQL-Server, випустивши web-звіт про результати тесту. У разі якщо ці результати можуть бути необхідними sp_makewebtask пропонує параметри для генерації web-звітів на вимогу користувача, включаючи варіанти планування роботи через планувальник завдань служби SQL Server’s Agent service.


В процесі виконання процедури up_AutoTest в Web-звіті за результатами тесту (Test Results Web) буде видно, що процедура up_lookupPrice не виконала створений тест з тією ж самою помилкою в призначеному для користувача коді (10), як і раніше – і, таким чином, попереджає про відсутність таблиці Products table. Три інші процедури виконали свої тести, повертаючи код виконання (0) (без помилок) програмі виклику. Sproc_1 не містить вихідних параметрів (звідси відсутність прапорця – None flag – в стовпці реальних вихідних значень – the Actual Output Value column, тоді як Sproc_2 і Sproc_3 передають назад очікувані вихідні значення.


Використання Up_AutoTest


У процесі використання автоматизованого тестування потрібно пам’ятати, що ефективність тесту залежить як від створених тестових випадків, так і від виконуваних тестових процедур. Чи викликають тестові випадки очікувану лінію поведінки (наприклад, коригування поля) процедури? Якщо ні, то необхідно переглянути використовувані вхідні значення або провести попереднє тестування стану бази даних (наприклад, значень в стовпцях, дозволів доступу). Виходять чи очікувані дані (повертаються значення, вихідні параметри, зміни в таблиці)? Поки процедура не здійснює автоматичну перевірку результату роботи оператора вибору SELECT або змін у таблиці, що відбулися в результаті виконання установок INSERT, UPDATE або DELETE. Якщо ці перевірки важливі для досягнення мети тесту, можна виконати їх вручну або розширити процедуру up_AutoTest для виконання цієї роботи.


Застосовуйте на практиці


Тепер ви можете автоматизувати модульні тести для зберігання процедур. Ця структура і асоціативний тест-драйвер up_AutoTest, який використовується в прикладах цієї статті, підтримує багато повторюються тестові сценарії. Використання up_AutoTest допоможе заощадити час модульного тестування нової процедури, перевірки процедури, створеної раніше, налагодження процедур і виконання зворотних тестів для виявлення небажаних побічних ефектів або процедур модульного тестування через базу даних в цілому або прикладну систему. Якщо є бажання побачити на практиці, наскільки ефективним буде автоматизоване тестування, скопіюйте і розмістіть листинги цієї статті в аналізаторі Query Analyzer і проведіть тест-драйв для up_AutoTest. Результат вас не розчарує.



Схема № 1. Робочі кроки автоматизованої тестової процедури up_AutoTest лістингу 4.



  1. Відновити тестовий варіант з таблиці tabTestValues;
  2. Оголосити і ініціалізувати тестові змінні в таблиці tabTestValues;
  3. Створити курсор, щоб відновити tabTestValues ​​для кожного параметра;
  4. Динамічно створити список параметрів для тестованої процедури;
  5. Динамічно побудувати запит про виконання для даної процедури;
  6. Динамічно виконати процедуру;
  7. Помістити вихідні результати тесту в таблицю tabTestResults;
  8. Повертатися до кроку 1 до тих пір, поки всі тестові випадки не будуть оброблені;
  9. Оновити таблицю tabTestResults відповідно до результатів тесту;
  10. Створити web-звіт по таблиці tabTestResults.


Таблиця 1. Стовпці значень і функцій в таблиці tabTestValues.



























Стовпець   Функція 
proc_name Називає тестовану процедуру.
proc_type Відстежує, чи має процедура вихідний параметр.
test_number Відстежує виконується тест.
parm_name Називає будь-які параметри процедури.
data_type Тип даних параметра.
parm_type Розрізняє вхідні і вихідні параметри
test_value Значення, яке визначено для параметра в поточному тесті.

Лістинг 1. Код, що створює таблицю tabTestValues.





CREATE TABLE tabTestValues (
  proc_name varchar(24), proc_type char(1), test_number int,
  parm_name varchar(32), data_type varchar(15), parm_type
  varchar(12), test_value varchar(24)
)


Лістинг 2. Код для заповнення таблиці tabTestValues.

INSERT tabTestValues  VALUES (“sproc_1”, “I”, 1,


 “first_name”, “varchar(24)”, “input”, “jerry”)

INSERT tabTestValues  VALUES (“sproc_1”, “I”, 1,


 “last_name”, “varchar(24)”, “input”, “feldsein”)

INSERT tabTestValues  VALUES (“sproc_1”, “I”, 1,


 “age”, “int”, “input”, 54)GO

INSERT tabTestValues  VALUES (“sproc_2”, “O”, 2,


 “product”, “varchar(24)”, “input”, “mink”)

INSERT tabTestValues  VALUES (“sproc_2”, “O”, 2,


 “quantity”, “int”, “input”, 2)

INSERT tabTestValues  VALUES (“sproc_2”, “O”, 2,


 “unit_price”, “money”, “input”, 500.30)

INSERT tabTestValues  VALUES (“sproc_2”, “O”, 2,


 “total_cost”, “money”, “output”, 1000.60)GO

INSERT tabTestValues  VALUES (“sproc_3”, “O”, 3,


 “last_name”, “varchar(15)”, “input”, “jones”)

INSERT tabTestValues  VALUES (“sproc_3”, “O”, 3,


 “error_message”, “varchar(30)”, “output”, “Logic Error: 100 “)GO

INSERT tabTestValues  VALUES (“up_lookupPrice”, “O”, 4,


 “@product_id”, “int”, “input”, 7)

INSERT tabTestValues  VALUES (“up_lookupPrice”, “O”, 4,


 “@product_price”, “money”, “output”, 29.95)


Лістинг 3. Збережені процедури, на які посилається таблиця tabTestValues.





CREATE PROCEDURE sproc_1
 @first_name varchar(24) = “dan”,
 @last_name varchar(24) = “johnson”,
 @age int = 40
AS
 PRINT “Input value 1: ” + @first_name
 PRINT “Input value 2: ” + @last_name
 PRINT @age
 RETURN 0
GO
CREATE PROCEDURE sproc_2
 @product varchar(24) = “mens winter coat”,
 @quantity int = 1,
 @unit_price money = 22.54,
 @total money output
AS
 SELECT @total = @quantity * @unit_price
 RETURN 0
GO
CREATE PROCEDURE sproc_3
 @last_name varchar(15) = “lastname”,
 @error_message varchar(30) output
AS
 SELECT @error_message = “Logic Error: 100”
 RETURN 0
GO
CREATE PROCEDURE up_lookupPrice
  @product_id int,
  @product_price money OUTPUT
AS
 IF object_id(“dbo.products”) IS NULL
    RETURN 10                                       /* Data table does not exist. */
 SELECT @product_price = product_price
   FROM Products WHERE product_id = @product_id
 IF @@rowcount = 0                         /* row not found */
    RETURN 11
 IF @product_price IS NULL                /* unit_price is null */
    RETURN 12
 RETURN
GO


Лістинг 4. Процедура up_AutoTest.





CREATE PROCEDURE up_autotest
AS
 DECLARE @test_number int, @number_tests int
 SET @test_number = 0
 
/* Get number of tests to run. */
 SET @number_tests = (SELECT max(test_number) FROM tabTestValues )
 
/* For each test… */
 WHILE @test_number < @number_tests
  BEGIN
 
/* Declare and initialize. */
  SET @test_number = @test_number + 1
  DECLARE @proc_type char(1), @parm_type varchar(12), @test varchar(4),
   @parm_name varchar(12), @data_type varchar(15), @sproc_name varchar(24),
   @parm_value varchar(24), @parm_list varchar(128), @tabTestValues  varchar(128), @count
      tinyint
  SET @count = 0
  SET @parm_list = NULL
  SET @tabTestValues  = NULL
 
/* Get test values.*/
  DECLARE curParms cursor FOR
   SELECT proc_name, proc_type, test_number, parm_name, data_type, parm_type, test_value
   FROM tabTestValues
   WHERE test_number = @test_number
  FOR read only
  OPEN curParms
  FETCH curParms INTO @sproc_name, @proc_type, @test_number,
    @parm_name, @data_type, @parm_type, @parm_value
 
/* Build parameter list.*/
  WHILE (@@fetch_status = 0)
    BEGIN                                   /* For each test… */
 
  /* Verify that @parm-list is long enough to hold all parameters. */
    IF len(@parm_list) > 128
      RETURN 10                             /* Parameter list exceeds 128 characters. */
    IF @count = 0                       /* First parameter doesn”t need a comma. */
     BEGIN
       SET @parm_list = @parm_value
       SET @tabTestValues  = @parm_name + ” = ” + @parm_value
     END
     ELSE
     IF @parm_type = “output”
      SET @parm_list = @parm_list + “, @output_parm OUTPUT ”
     ELSE
     BEGIN
       SET @parm_list = @parm_list + “, ” + @parm_value
       SET @tabTestValues  = @tabTestValues  + “, ” + @parm_name + ” = ” + @parm_value
     END
     FETCH curParms INTO @sproc_name, @proc_type, @test, @parm_name, @data_type,
       @parm_type,  @parm_value
     SET @count = @count + 1
     END
 
/* Close cursor. */
  CLOSE curParms
  DEALLOCATE curParms
 
/* Build procedure query. */
  DECLARE @query varchar(500)
  SET @query = “”
/* Procedures with output parameters require different format. */
  IF @proc_type = “O”
    BEGIN
     SET @query =
      ” DECLARE @return_status int, @output_parm ” + @data_type +
      ” EXECUTE @return_status = ” + @sproc_name + ” ” + @parm_list +
 
         ” INSERT TabTestResults (test_number, proc_name, tabTestValues , return_value,
              expect, output )” +
         ” values (” + @test + “,””” + @sproc_name + “””,””” + @tabTestValues  + “””,
              @return_status, ”
 
  IF @data_type = “int” or @data_type = “money” 
     /* @parm_value must handle strings as well as numbers. */
       BEGIN              /* Data type is numeric. */
       SELECT @query = @query +
         ” str(” + @parm_value + “,10,2) ,” + ” str(@output_parm,10,2)) ”
       END
       ELSE               /* Data type is varchar. */
       SELECT @query = @query + “””” + @parm_value + “””,@output_parm )”
    END
 
  ELSE                    /* Procedure has no output parameter. */
    BEGIN
         SET @query =
             ” DECLARE @return_status int ” +
             ” EXECUTE @return_status = ” + @sproc_name + ” ” + @parm_list +
             ” INSERT tabTestResults (output, test_number, proc_name,
             return_value, expect, tabTestValues ) ” +
             ” values (” + “””None””,” + @test + “,””” + @sproc_name + “””,
             @return_status, ” + “””None””” + “, “”” + @tabTestValues  + “””)”
    END
 
/* Execute procedure dynamically. */
    EXEC (@query)
 
 END
 
/* Compute/store test results: */
    UPDATE TabTestResults
    SET output =
              CASE
                 WHEN output IS NULL
                     THEN “Missing”
                 ELSE
                     output
              END,
                 test_result =
              CASE
                 WHEN output = “None” and return_value = 0
                      THEN “Passed”
                      WHEN expect = output and return_value = 0
                      THEN “Passed”
              ELSE
                    “Failed”
              END
 
/* Produce Web report for test results. */
  EXEC sp_makewebtask
    @outputfile = “c:webtask.htm”,
    @resultstitle = “Test Results”,
    @query = “SELECT test_number AS “”Test””,
                                    proc_name AS “”Procedure””,
                                    tabTestValues  AS “”Test Values””,
                                    return_value AS “”Return Value””,
                                    expect AS “”Expected Value””,
                                    output AS “”Actual Output Value””,
                                    test_result AS “”Test Result””         
                     FROM TabTestResults
                ORDER BY test_result, proc_name”
  RETURN 0
 

Лістинг 5. Код, що створює таблицю tabTestResults.





CREATE TABLE tabTestResults (
  test_number varchar(4),
  proc_name varchar(24),
  return_value int,
  expect varchar(24),
  output varchar(24),
  test_result varchar(8),
  tabTestValues  varchar(128) )

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*