ЛАБОРАТОРНА РОБОТА № 10-11
БІБЛІОТЕКА МОДУЛЬНОГО ТЕСТУВАННЯ JUNIT

 

Мета: набути навичок роботи з бібліотекою модульного тестування JUnit.

 

Довідка

На сучасному етапі розвитку підходів до розробки програмних систем здійснення при цьому лише навантажувального тестування або тестування продуктивності систем є недостатнім. Причина цього полягає у поширенні об’єктно-орієнтованого підходу при проектуванні та програмуванні. У зв’язку із цим не досить ефективне виявлення помилок виключно у вже розробленому програмному продукті. Це, зокрема, йде всупереч центральній тезі актуального на сьогодні ітераційного підходу до розробки, згідно якої на кожному умовно виділеному етапі процесу розробки має здійснюватися певна перевірка поточних здобутків [1].

При розробці на основі тестування, так званому TDD-підходу (Test-driven Development), спочатку створюються тести для певних компонентів або модулів системи, а вже потім – самі модулі, що вдовольняють тестам [2]. Поширеним засобом створення названих тестів є бібліотека модульного тестування JUnit. Центральними при цьому є поняття модульного тесту (unit-тесту) та тестованого модуля.

Під unit-тестом будемо розуміти Java-метод у складі певного Java-класу, позначений анотацією @Test та призначений для перевірки окремого тестованого модуля – Java-методу іншого Java-класу, правильність роботи якого ми намагаємося перевірити.

При створенні та використанні модульних тестів будемо користуватися середовищем NetBeans 8.0 IDE. При цьому варто пам’ятати, що в проекті, створюваному в зазначеному середовищі, класи тестів мають групуватися за каталогами, які, в свою чергу, мають знаходитися в каталозі "Тести" проекту.

Ідея unit-тестування полягає у виконанні перевірки програмних модулів індукційним шляхом (знизу вверх) – від базових (вихідних) – до похідних (результуючих) модулів. Це дозволяє спростити в процесі розробки внесення змін як в Java-методи (при рефакторингу програмного коду), так і у відповідні unit-тести.

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

На основі бібліотеки JUnit, окрім непараметризованого та параметризованого unit-тестувань, може бути здійснене також і тестування у пакетному режимі – suite-тестування. Призначення останнього – автоматизація безпосередньо процесу тестування, що дасть змогу одержувати результуючі дані за результатами перевірок кількох модулів.

В даній лабораторній роботі використовувалась версія бібліотеки JUnit–4.11, що підтримується середовищем розробки NetBeans 8.0. Використання актуальної бібліотеки (http://junit.org/) дасть змогу користуватися актуальними версіями методів та анотацій при виконанні роботи.

Для успішного виконання роботи до Java-проекту, створеного в середовищі NetBeans 8.0, слід підключити бібліотеку JUnit, представлену jar-архівом junit-4.11.jar. Для цього слід виконати наступне: у властивостях проекту в категорії Бібліотеки на вкладці Компілювати вказати з використанням елементу керування Додати файл JAR/папку вказати шлях до фактичного розміщення архіву junit-4.11.jar.

Модульне тестування ґрунтується на наступних принципах [3]:

– окремий модульний тест має виконуватися незалежно від інших unit-тестів;

– порядок виявлення помилок в модулях, а також порядок появи повідомлень про помилки мають відповідати послідовності виконання модульних тестів;

– при тестуванні має існувати можливість вибору модульного тесту, що підлягає виконанню;

Розробники бібліотеки JUnit (Kent Beck, Erich Gamma) керувалися наступними принципами:

– використання unit-тестів має бути обґрунтованим потребами процесу розробки;

– модульні тести мають бути актуальними при повторному їх використанні.

Відмітні риси JUnit:

– використання анотацій у якості засобів ініціалізації та налаштування тестів (@After, @Before, @Test...);

– використання assert-методів для перевірки результатів тестувань;

– інтеграція бібліотеки з популярними комплексами засобів автоматизації створення програмних проектів (Ant та Maven), а також із поширеними середовищами розробки (IDE, Integrated Development Environment) – Eclipse, NetBeans, JBuilder та ін.

Модульне тестування також може здійснюватися і для проектів, створюваних із використанням інших мов програмування: JavaScript JSUnit, C++ CPPUnit, PHP PHPUnit та ін.

Призначення анотацій JUnit [4]:

– @After – вивільнення ресурсів (пам'яті), виділених для використання @Before-анотованих методів, по завершенні тестування (анотовані методи мають бути public void);

– @AfterClass – призначення те саме, що й у випадку @After-анотації, але використовується для вивільнення ресурсів, виділених з метою створення передумов до виконання декількох модульних тестів (анотовані методи мають бути public static void);

– @Before – вказування об’єктів, що підлягають створенню, а також методів, що підлягають використанню, передуючи тестуванню. Анотація @Before є засобом створення передумов до здійснення тестування (анотовані методи мають бути public void);

– @BeforeClass – призначення те саме, що й у випадку @Before-анотації, але використовується для створення однакових передумов до декількох модульних тестів – методів, позначених @Test-анотаціями (анотовані методи мають бути public static void);

– @Ignore – скасування @Test-анотації. Це означає, що модульний тест, помічений як @Ignore @Test, виконанню не підлягає. Анотація @Ignore також може бути параметризованою (із зазначенням причини скасування @Test-анотації) – наприклад @Ignore("not ready yet") @Test ...;

– @Parameterized.Parameters – вказування статичного методу, що повертає набір тестових даних, необхідних для проведення параметризованого тестування. При цьому необхідно попередньо використовувати анотацію @RunWith (Parameterized.class). Параметризоване тестування доцільно виконувати за потреби перевірки тестованого модуля на множині вихідних даних;

– @Rule – позначення об’єктів, призначених до використання в модульному тесті;

– @RunWith – вказування класу, призначеного для виклику з метою виконання модульного тесту. Наприклад, анотація @RunWith (Parameterized.class) сигналізує про здійснення параметризованого тестування, а анотація @RunWith (Suite.class) – про тестування в пакетному режимі;

– @Suite – виконання модульних тестів в пакетному режимі (з метою автоматизації). Для вказування модульних тестів, що мають бути включені до тестового набору. Класи, що містять елементи тестового набору, вказуються у якості аргументів анотації @Suite.SuiteClasses;

– @Test – позначення модульних тестів. Анотація @Test може бути параметризованою (із ключовим словом timeout), де в якості параметру вказується значення часу (в мілісекундах), по завершенні якого тест буде вважатися непройденим (failed), тобто параметризація здійснюється з метою встановлення часових обмежень на тестування. Наприклад, параметризованою анотацією @Test (timeout=1000) можна задати обмеження на час виконання тесту в 1 с.

Шаблон модульного тесту з використанням анотації @Test наведено в лістингу 1.1.

 

//Лістинг 1.1 Структура модульного тесту

//імпортування необхідних JUnit–пакетів

import org.junit.Test;

import static org.junit.Assert.*;

public class A {  //клас, що містить

//модульний тест

@Test              //позначення нижченаведеного методу

//як модульного тесту

public void some_method() {

//створення екземпляру класу, що містить

//тестований модуль

B b = new B();

//привласнення змінній оціночного (очікуваного)

//значення-результату роботи тестованого модуля

double estimated_val = 12.3;

//одержання фактичного результату

//роботи тестованого модуля

double result = b.getResult();

//порівняння фактичного та оціночного значень

//шляхом використання assert-методу

assertEquals(estimated_val, result,0);

 

Для лістингу 1.1 результат тестування буде успішним (passed), якщо фактичне та оціночне значення рівні. Якщо ні – результат тестування буде ідентифікований як невдалий (failed).

Основні класи бібліотеки JUnit наведено в табл. 1.1 Деякі assert-методи наведено в табл. 1.2.

 

Таблиця 1.1 – Базові класи бібліотеки JUnit

Класи

Призначення

Assert

містить множину assert-методів, призначених для створення unit-тестів

Runner

надає засоби виконання параметризованого тестування

Suite

дозволяє здійснювати тестування в пакетному режимі

 

Таблиця 1.2 – assert-методи бібліотеки JUnit

Методи

Аргументи та призначення

static void assertEquals(

double expected,

double actual,

double delta);

expected – очікуване значення;

actual – фактичне значення;

delta – допустиме відхилення (девіація) між фактичним та оціночним значеннями

static void assertArrayEquals( char[] expected,char[] actual);

expected – масив оціночних значень;

actual – масив фактичних значень

static void assertTrue( boolean condition);

перевірка істинності припущення

static void assertFalse( boolean condition);

перевірка хибності припущення

 

На практиці, проте, перевірка методів, у тілі яких не виконується створення екземплярів інших класів, є малопоширеною. Процес розробки, як правило, ґрунтується на використанні певних шаблонів проектування, коли важко уникнути створення композитних об’єктів на основі декількох базових класів [5]. Розробники при цьому можуть мати у розпорядженні лише інтерфейси базових класів, що унеможливлює здійснення модульного тестування виключно з використанням бібліотеки JUnit. Можливим шляхом вирішення такої проблеми є використання при тестуванні mock-об'єктів, що являють собою фіктивні реалізації інтерфейсів.

Для створення mock-об'єктів при виконанні роботи використаємо бібліотеку JMock. Існують, однак, і чимало альтернативних бібліотек, що базуються на тій самій ідеї, – RMock, Mockito тощо.

Для користування засобами JMock при виконанні роботи до Java-проекту, створеного в середовищі NetBeans 8.0, слід підключити наступні jar-архіви: hamcrest-core-1.3.jar, hamcrest-library-1.3.jar, jmock-2.6.0.jar та jmock-junit4-2.6.0.jar.

Розглянемо приклад. Нехай розробник оперує двома сутностями – інтерфейсом постачальника продукції Supplier (лістинг 1.2) та класом складу продукції Storage (лістинг 1.3). Розглянемо інтерфейс Supplier як інтерфейс базового класу – класу, від результатів роботи методу getData якого залежить результат роботи методу getSupply класу Storage.

 

//Лістинг 1.2. Інтерфейс базового класу

package foo;

public interface Supplier {

//метод одержання інформації відносно продукції

public String getData();

}

 

//Лістинг 1.3. Клас із тестованим модулем getSupply

package foo;

public class Storage {

public void addSupplier(Supplier obj) {

this.obj = obj;

}

public String getSupply() {

return "supplied: " + obj.getData();

}

private Supplier obj;

}

 

За умови відсутності в розробника класу, що реалізує інтерфейс Supplier (лістинг 1.2), неможливо здійснити unit-тестування методу getSupply класу Storage лістингу 1.3, оскільки для цього необхідно створити екземпляр відсутнього класу.

Наприклад, якщо створити модульний тест методу getSupply, наведений в лістингу 1.4, то в результаті запуску на виконання такого тесту (Shift+F6) виникне помилка java.lang.NullPointerException.

 

//Лістинг 1.4. Модульний тест методу getSupply

package storage;

import foo.*;

import org.junit.Test;

import static org.junit.Assert.*;

public class UnitTester {

@Test

public void testStorage() {

assertEquals(new Storage().getSupply(),

"supplied: ");

}

}

 

Названа помилка полягає у неможливості одержати посилання на об’єкт класу, який реалізує інтерфейс Supplier – з метою виклику методу getData. Аби уникнути цього, можна скористатися mock-об'єктом – фіктивним об’єктом класу-реалізації інтерфейсу Supplier, який нібито існує (лістинг 1.5).

 

//Лістинг 1.5. Модульний тест методу getSupply на основі mock- об'єкту

package storage;

import foo.*;

import org.junit.*;

import static org.junit.Assert.*;

import org.junit.runner.*;

import org.junit.runners.*;

11

import org.jmock.Mockery;

import org.jmock.Expectations;

import org.jmock.integration.junit4.JUnitRuleMockery;

import org.jmock.integration.junit4.JMock;

@RunWith(JMock.class)

public class StorageTester {

@Rule public JUnitRuleMockery context =

new JUnitRuleMockery();

@Test

public void testStorage() {

final Supplier supplier =

context.mock(Supplier.class);

Storage storage = new Storage();

storage.addSupplier(supplier);

context.checking(new Expectations() {

{oneOf (supplier).getData();}

});

result = storage.getSupply();

context.assertIsSatisfied();

assertEquals(result, expected);

}

private static final String expected = "supplied: ";

private String result;

}

 

У лістингу 1.5 анотацією @RunWith(JMock.class) вказується, що виконання тесту має здійснюватися на основі методів класу JMock. На основі класу JUnitRuleMockery одержується контекст – підґрунтя для створення mock-об'єктів згідно інтерфейсу Supplier за рахунок використання методу mock. За рахунок використання методу checking перевіряємо твердження (наше припущення) щодо можливого сценарію застосування створеного mock-об'єкту (виклик методу getData). Методом assertIsSatisfied перевіряємо істинність припущення, а методом assertEquals – рівність результату роботи тестованого модуля getSupply очікуваному результату expected.

 

Порядок виконання роботи

1. Створення unit-тестів та проведення непараметризованого тестування.

1.1 Створити в NetBeans 8.0 IDE новий Java-проект: меню Файл > Створити проект > Java > Java додаток. В якості назви проекту вказати Lab1.1. В проекті створити модульні тести (натиснувши правою клавішею миші на створеному проекті): меню Файл > Створити файл > Модульні тести > Модульний тест JUnit. В якості назви пакету вказати test, а в якості назви створюваного класу – Tester. У результаті виконання зазначених дій створений проект буде містити пакет test із класом Tester – шаблоном модульного тесту. Вміст класу-шаблону слід відредагувати у відповідності до лістингу 1.6.

 

//Лістинг 1.6. Клас, що містить модульний тест

package test;

import org.junit.Test;

import static org.junit.Assert.*;

public class Tester {

@Test

public void test() {

Calc calc = new Calc(3,3);

assertEquals(calc.getResult(), 9, 0);

}

}

 

В лістингу 1.6 наведено модульний тест, представлений методом test класу Tester, про що свідчить відповідна анотація @Test. У тілі методу test створюється екземпляр класу Calc, що містить тестований метод getResult. Шляхом використання assertEquals-методу у тілі методу test перевіряємо, чи рівний результат роботи методу 9, – для вхідних даних, переданих через конструктор класу Calc.

1.2 Підключити архів junit-4.11.jar до створеного проекту.

1.3 Створити тестований модуль. Для цього необхідно в пакеті вихідних кодів створити пакет calc, що буде містити клас Calc із тестованим модулем getResult. Клас Calc наведено в лістингу 1.7.

 

//Лістинг 1.7. Клас із тестованим модулем

package calc;

public class Calc {

public Calc(int x, int y) {

a = x;

b = y;

}

public int getResult() {

return a*b;

}

private int a, b;

}

 

1.4 Імпортувати клас Calc до класу Tester із модульним тестом:

import calc.Calc;

1.5 Здійснити непараметризоване unit-тестування в середовищі NetBeans 8.0. Для цього слід запустити на виконання файл класу Tester (Shift+F6).

1.6 Замінити рядок

assertEquals(calc.getResult(), 9, 0);

класу Tester рядком

assertEquals(calc.getResult(), 8, 1);

Після того – рядком

assertEquals(calc.getResult(), 10, 0.5);

Пояснити вплив значення третього параметру методу assertEquals – delta – на результати тестування.

1.7 Створити умови, при яких результати проведення модульного тесту будуть залежати також і від супутніх тестуванню часових витрат. Для цього слід замінити анотацію @Test параметризованим аналогом @Test (timeout=...), де замість ... слід задати обмеження (в мілісекундах) на час виконання тесту. Слід також доповнити тіло методу getResult класу Calc штучно введеною часовою затримкою, шаблон реалізації якої наведено в лістингу 1.8.

 

//Лістинг 1.8. Шаблон часової затримки

try {

Thread.sleep(some_delay);

}

catch(InterruptedException e) {

e.printStackTrace();

}

 

В лістингу 1.8 аргумент some_delay методу sleep являє собою або безпосередньо значення затримки в мілісекундах, або ініціалізовану цим значенням змінну.

1.8 Поекспериментувати зі значеннями аргументу sleep-методу класу Calc та timeout-аргументу параметризованої @Test-анотації для модульного тесту класу Tester. Розглянути при цьому три випадки: some_delay > timeout; some_delay < timeout та some_delay == timeout. Прокоментувати одержувані при цьому лістинги про результати unit-тестування. Лістинги та коментарі до них навести у звіті про виконання лабораторної роботи.

1.9 Варіант 1. Перевірити рівність двох масивів символьних значень шляхом використання assert-методу. Один з масивів має бути полем класу Calc, а інший – полем класу Tester.

1.10 Варіант 2. Перевірити рівність двох масивів цілих значень шляхом використання assert-методу. Один з масивів має бути полем класу Calc, а інший – полем класу Tester.

1.11 Варіант 1. В пакеті calc створити клас, що містить метод обчислення факторіалу від n (тестований модуль). В пакеті test створити клас, що містить модульний тест для зазначеного методу. Провести unit-тестування методу обчислення факторіалу та продемонструвати результати тестування.

1.12 Варіант 2. В пакеті calc створити клас, що містить метод одержання чисел Фібоначчі (0,1,1,2,3,5,…). В якості єдиного аргументу методу передається кількість чисел, що слід згенерувати. В пакеті test створити клас, що містить модульний тест для зазначеного методу. Провести unit-тестування методу одержання чисел Фібоначчі та продемонструвати результати тестування.

2 Параметризование unit-тестування.

2.1 Провести параметризоване тестування методу встановлення, що задане число є простим – методу definePrime класу Prime пакету prime (лістинг 1.9). Відповідний параметризований модульний тест наведено в лістингу 1.10. Проведення параметризованого unit-тестування замість непараметризованого дозволить перевірити зазначений метод на множині вихідних даних.

 

//Лістинг 1.9. Клас тестованого модуля

package prime;

public class Prime {

public boolean definePrime(int n) {

prime = n;

for(int i=2; i<prime/2; ++i) {

if(prime%i == 0)

return false; //число не є простим

}

return true; //число є простим

}

private int prime;

}

 

//Лістинг 1.10. Реалізація параметризованого unit-тесту

package test;

import prime.Prime;

import org.junit.runner.*;

import org.junit.runners.*;

import org.junit.Test;

import static org.junit.Assert.*;

import java.util.Arrays;

import java.util.Collection;

@RunWith(Parameterized.class)

public class PrimeTester {

public PrimeTester (int n, boolean expected) {

prime = new Prime();

this.n = n;

this.expected = expected;

}

@Parameterized.Parameters

public static Collection getNumbers() {

return Arrays.asList(new Object[][] {

{1, true},

{3, true},

{6, false},

{7, true}

});}

@Test

public void test() {

assertEquals(expected, prime.definePrime(n));

}

private int n;

private boolean expected;

private Prime prime;

}

 

Зверніть увагу, що у лістингу 1.10 формат елементів списку-результату роботи статичного методу getNumbers визначається сигнатурою конструктора класу PrimeTester, що містить модульний тест. Формат елементів списку варто трактувати наступним чином: спочатку зазначається ціле число, для якого ми встановлюємо, чи є воно простим – число передається у якості аргументу методу definePrime класу Prime; потім зазначається наше власне припущення щодо того, чиє число простим (true – є, false – не є). Множину булевих значень, що фігурують у якості складових елементів списку (тип Collection) у відповідності до формату конструктора класу PrimeTester, варто розглядати як множину зразкових значень, яким мають дорівнювати результати роботи тестованого методу definePrime на множині вихідних значень {1,3,6,7}.

2.2 Провести параметризоване тестування на основі лістингу 1.10, замінивши елемент списку {6, false} елементом {6, true}. Прокоментувати результат тестування. Коментарі до результатів навести у звіті.

2.3 Варіант 1. Створити у класі Prime метод встановлення парності (непарності) цілого числа. Створити відповідний модульний тест та виконати параметризоване тестування методу.

2.4 Варіант 2. Створити у класі Prime метод обчислення залишку від ділення одного цілого числа на інше. Створити відповідний модульний тест та виконати параметризоване тестування методу.

2.5 Варіант 1. В пакеті prime створити клас Gray, в якому реалізувати метод обчислення коду Грея для цілих чисел від 0 до 2n-1 n , де n = 3 – кількість двійкових розрядів коду. Створити відповідний модульний тест та виконати параметризоване тестування методу.

2.6 Варіант 2. В пакеті prime створити клас Nsd, в якому реалізувати метод обчислення найбільшого спільний дільника (НСД) для двох цілих чисел. Створити відповідний модульний тест та виконати параметризоване тестування методу. Рекомендації до виконання завдань 1.2.2.5 і 1.2.2.6 наведено в додатку А.

2.7 Здійснити unit-тестування методу getSupply (лістинг 1.3) на основі лістингу 1.5. Пояснити результат тестування.

2.8 Створити модульний тест для методу getSupply, в якому би не використовувалися mock-об’єкти. Для цього потрібно також створити клас, що реалізує інтерфейс Supplier (лістинг 1.2).

3 Тестування у пакетному режимі (suite-тестування).

3.1 На основі модульних тестів (unit-тестів), одержаних при виконанні завдань згідно варіантів, здійснити suite-тестування. Прокоментувати результати тестування у пакетному режимі.

Зразок класу, що містить suite-тест, наведено в лістингу 1.11.

 

//Лістинг 1.11. Клас TestSuite із suite-тестом

import org.junit.runner.RunWith;

import org.junit.runners.Suite;

@RunWith(Suite.class)

@Suite.SuiteClasses({ // suite-тест

class_1.class, // клас із unit-тестами

class_2.class // клас із unit-тестами

})

public class TestSuite {}

 

Питання для самостійної перевірки

1. Специфіка TDD-підходу до розробки програмних систем.

2. Призначення та принципи модульного тестування.

3. Відмінні риси бібліотеки модульного тестування JUnit.

4. Поняття модульного тесту та тестованого модуля. Послідовність дій при створенні модульного тесту.

5. Анотації JUnit. Призначення assert-методів бібліотеки JUnit. Приклади використання.

6. Призначення mock-об'єктів.

7. Прокоментувати результат тестування (failed або passed) стосовно рядка коду лістингу 1.6 assertEquals(calc.getResult(), 9, 0);. Чи буде результат таким самим у випадку заміни наведеного рядка рядком assertEquals(calc.getResult(), 8, 1);?

8. Обґрунтувати доцільність використання анотації @Test (timeout=var) замість анотації @Test.

9. Призначення параметризованого тестування. Навести приклад.

10. Доцільність suite-тестування. Навести приклад.

 

Протокол

1. Титульний лист.

2. Мета роботи.

3. Лістинги кодів параметризованих unit-тестів згідно

варіанту та коментарі до них. Лістинг коду результуючого suite-тесту.

4. Відповіді на контрольні питання.

 

Список рекомендованих інформаційних джерел

1. Ларман К. Применение UML 2.0 и шаблонов проектирования. Введение в объектно-ориентированный анализ, проектирование и итеративную разработку / К. Ларман; пер. с англ.; под ред. А.Ю. Шелестова. – [3-е изд.]. – М.: Вильямс, 2013. – 736 с.

2. Freeman S. Growing Object-Oriented Software, Guided by Tests / S. Freeman, N. Pryce. – NY.: Addison-Wesley, 2009. – 384 p.

3. Tahchiev P. JUnit in Action / P. Tahchiev, F. Leme, V. Massol, G. Gregory. – [2nd ed.]. – Stamford, CT, The USA: Manning Publications Co., 2010. – 504 p.

4. Package org.junit [Електронний ресурс] – Режим доступу: http://junit.sourceforge.net/javadoc/org/junit/package-summary.html. – Заголовок з екрану.

5. Гранд М. Шаблоны проектирования в Java / М. Гранд; пер. с англ. С. Беликовой. – М.: Новое издание, 2004. – 559 с.