Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

среда, 16 марта 2011 г.

Java for fun: Разрабатываем Fitnesse тесты #2

В прошлый раз мы начали писать свои фикстуры, но залезли немного в дебри. На самом деле наследоваться от Fixture тебе придется в редких случаях - тогда, когда не поможет базовый набор фикстур (уже разработанный для тебя) - это fit.ColumnFixture, fit.ActionFixture и fit.RowFixture.

Начнем с fit.ActionFixture. Для этого исходную табличку
 
!|calculator.fixtures.CalculatorActionFixture|
|Операция|Показания экрана после выполнения операции|
|С|0|
|1|1|
|С|0|

модифицируем так
 
!|fit.ActionFixture|
|start|calculator.fixtures.CalculatorActionFixture|
|enter|button|С|
|check|display|0|
|enter|button|1|
|check|display|1|
|enter|button|С|
|check|display|0|

и проделаем то же с каждой табличкой типа CalculatorActionFixture.

В скомпилированном виде табличка выглядит так


Ну, что очень даже ничего. Думаю, заказчик не будет сильно против слегка изменившегося формата.

Если теперь попытаться запустить такой тест, то Fitnesse подскажет нам, что в нашем классе-фикстуре не хватает некоторых методов.


Добавим их!

package calculator.fixtures;

import fit.Fixture;

public class CalculatorActionFixture extends Fixture {
 
    public String display() {
        return "";  
    }  
 
    public void button(String buttons) { 
    } 
}

Если теперь запустить на выполнение тест, то картинка изменится


А все потому что мы ожидаем (expect) одно, а получаем (actual) другое.

Принцип работы фикстуры fit.ActionFixture прост. Табличка имеет формат

!|fit.ActionFixture|
|start|package.ClassName|
|enter|method1|someData1|
|check|method2|someData2|
|press|method3|someData3|

Тег start - говорит о том, какой класс будет использоваться. Этот класс должен наследовать fit.Fixture и содержать в себе ряд методов method1, method2 и method3, сигнатуры которых определяются по тегам enter/check/press предшествующим им:
  • для enter - public void method1(String data); // вводим данные в фикстуру
  • для check - public String method2(); // тут ActionFixture проведет сравнение того, что указано в таблице и того что вернет method2
  • для press - public void method3(); // просим фикстуру сделать что-то

Теперь нам надо некий калькулятор. Допустим его программеры сделали таким

package calculator.fixtures;

import java.math.BigDecimal;

public class Calculator {

    private static final String ZERRO = "0";
    private static final int NONE = 3;
    private static final int PLUS = 2;
    private static final int MINUS = 1;
    private String memory;
    private String current;
    private String display;
    private boolean willCleared;
    private int operation;

    public Calculator() {
        pressClear();
    }

    public String getDisplayValue() {
        return display;
    }
    
    private void pressNum(int number) {
        String numberString = String.valueOf(number);
        if (current.equals(ZERRO) || willCleared) {
            current = numberString;
        } else {
            current = current + numberString;
        }
        display = current;
        willCleared = false;
    }

    public void press1() {
        pressNum(1);
    }

    public void press2() {
        pressNum(2);
    }

    public void press3() {
        pressNum(3);
    }

    public void press4() {
        pressNum(4);
    }

    public void press5() {
        pressNum(5);
    }

    public void press6() {
        pressNum(6);
    }

    public void press7() {
        pressNum(7);
    }

    public void press8() {
        pressNum(8);
    }

    public void press9() {
        pressNum(9);
    }

    public void press0() {
        pressNum(0);
    }

    public void pressDot() {
        if (current.indexOf('.') == -1) {
            current = current + ".";
            display = current;
        }
    }

    public void pressClear() {
        display = ZERRO;
        memory = ZERRO;
        current = ZERRO;
        operation = NONE;
        willCleared = false;
    }

    public void pressPlus() {
        pressResult();
        operation = PLUS;
        willCleared = true;
    }

    public void pressMinus() {
        pressResult();
        operation = MINUS;
        willCleared = true;
    }

    public void pressResult() {
        switch (operation) {
            case PLUS:
                memory = plus(memory, current);
                break;
            case MINUS:
                memory = minus(memory, current);
                break;
            case NONE:
                memory = current;
                return;
        }
        operation = NONE;
        display = memory;
        current = ZERRO;
    }

    public String plus(String operand1, String operand2) {        
        return new BigDecimal(operand1).add(new BigDecimal(operand2)).toString();
    }
    
    public String minus(String operand1, String operand2) {        
        return new BigDecimal(operand1).add(new BigDecimal(operand2).negate()).toString();
    }

}

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

package calculator.fixtures;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class CalculatorTest {
    
    @Test
    public void testDefaultValue() {
        Calculator calculator = new Calculator();
        assertEquals("0", calculator.getDisplayValue());        
    }
    
    @Test
    public void testEnterNumber() {
        Calculator calculator = new Calculator();
        calculator.press1();
        calculator.press2();
        calculator.press3();
        calculator.press4();
        calculator.press5();
        calculator.press6();
        calculator.press7();
        calculator.press8();
        calculator.press9();
        calculator.press0();
        assertEquals("1234567890", calculator.getDisplayValue());
    }

    @Test
    public void testTryToFillMabyZerros() {
        Calculator calculator = new Calculator();
        calculator.press0();
        calculator.press0();
        calculator.press0();
        assertEquals("0", calculator.getDisplayValue());        
    }    
    
    @Test
    public void testTryToFillMabyZerrosWithDot() {
        Calculator calculator = new Calculator();
        calculator.press0();
        calculator.pressDot();
        assertEquals("0.", calculator.getDisplayValue());    
        calculator.press0();
        calculator.press0();        
        calculator.press0();
        assertEquals("0.000", calculator.getDisplayValue());        
    }    
    
    @Test
    public void testNoMultipleDots() {
        Calculator calculator = new Calculator();
        calculator.press0();
        calculator.pressDot();
        assertEquals("0.", calculator.getDisplayValue());
        calculator.pressDot();
        assertEquals("0.", calculator.getDisplayValue());
        calculator.press0();
        calculator.press0();        
        calculator.press0();
        assertEquals("0.000", calculator.getDisplayValue());        
    }    
    
    @Test
    public void testSumTwoNumbers() {
        Calculator calculator = new Calculator();
        calculator.press1();
        calculator.press2();
        calculator.press3();        
        calculator.pressPlus();
        calculator.press3();
        calculator.press2();
        calculator.press1();
        assertEquals("321", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("444", calculator.getDisplayValue());
    }
    
    @Test
    public void testSumThreeNumbers() {
        Calculator calculator = new Calculator();
        calculator.press1();
        calculator.press2();
        calculator.press3();        
        calculator.pressPlus();
        calculator.press3();
        calculator.press2();
        calculator.press1();        
        calculator.pressPlus();
        calculator.press1();
        calculator.press1();
        calculator.press1();
        calculator.pressResult();
        assertEquals("555", calculator.getDisplayValue());
    }
    
    @Test
    public void testMinTwoNumbers() {
        Calculator calculator = new Calculator();
        calculator.press3();
        calculator.press2();
        calculator.press1();    
        calculator.pressMinus();
        calculator.press1();
        calculator.press2();
        calculator.press3();
        assertEquals("123", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("198", calculator.getDisplayValue());
    }
    
    @Test
    public void testMinThreeNumbers() {
        Calculator calculator = new Calculator();
        calculator.press3();
        calculator.press2();
        calculator.press1();    
        calculator.pressMinus();
        calculator.press1();
        calculator.press2();
        calculator.press3();
        calculator.pressMinus();
        calculator.press1();
        calculator.press2();
        calculator.pressResult();
        assertEquals("186", calculator.getDisplayValue());
    }    
    
    @Test
    public void testMinusThenAfterPluss() {
        Calculator calculator = new Calculator();
        calculator.press3();
        calculator.press2();
        calculator.press1();    
        calculator.pressMinus();
        calculator.press1();
        calculator.press2();
        calculator.press3();
        calculator.pressPlus();
        calculator.press1();
        calculator.press2();
        calculator.pressResult();
        assertEquals("210", calculator.getDisplayValue());
    }
    
    @Test
    public void testMinTwoNumbersIfLessThan0() {
        Calculator calculator = new Calculator();
        calculator.press1();
        calculator.press2();
        calculator.press3();    
        calculator.pressMinus();
        calculator.press3();
        calculator.press2();
        calculator.press1();        
        assertEquals("321", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("-198", calculator.getDisplayValue());
    }
    
    @Test
    public void testSumNumWithDot() {
        Calculator calculator = new Calculator();
        calculator.press0();
        calculator.pressDot();
        calculator.press0();
        calculator.press0();        
        calculator.press1();                
        assertEquals("0.001", calculator.getDisplayValue());
        calculator.pressPlus();
        calculator.press2();
        calculator.press3();
        calculator.pressDot();
        calculator.press3();
        calculator.press0();        
        calculator.press2();
        assertEquals("23.302", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("23.303", calculator.getDisplayValue());
    }
    
    @Test
    public void testMinusNumWithDot() {
        Calculator calculator = new Calculator();
        calculator.press1();            
        assertEquals("1", calculator.getDisplayValue());
        calculator.pressMinus();
        calculator.press0();
        calculator.pressDot();
        calculator.press9();
        calculator.press9();        
        calculator.press9();
        assertEquals("0.999", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("0.001", calculator.getDisplayValue());
    }
    
    @Test
    public void testSumTwoNumbersAndPressResultTwice() {
        Calculator calculator = new Calculator();
        calculator.press1();
        calculator.press2();
        calculator.press3();        
        assertEquals("123", calculator.getDisplayValue());
        calculator.pressPlus();
        calculator.press3();
        calculator.press2();
        calculator.press1();
        assertEquals("321", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("444", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("444", calculator.getDisplayValue());
    }
    
    @Test
    public void testSumTwoNumbersAndTryToWriteSomeOtherNumber() {
        Calculator calculator = new Calculator();
        calculator.press1();
        calculator.press2();
        calculator.press3();        
        calculator.pressPlus();
        calculator.press3();
        calculator.press2();
        calculator.press1();
        assertEquals("321", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("444", calculator.getDisplayValue());
        calculator.press3();
        calculator.press2();
        calculator.press1();
        assertEquals("321", calculator.getDisplayValue());
        calculator.pressResult();
        assertEquals("321", calculator.getDisplayValue());
    }
}

Итак, есть калькулятор и есть фикстура, которая должна дергать его. Трудность в том, что интерфейсы у них разные - а значит фикстурка будет делать некоторые дополнительные преобразования.

Легко! Вот фикстура

package calculator.fixtures;

import fit.Fixture;

public class CalculatorActionFixture extends Fixture {
    
    private Calculator calculator = new Calculator();  
    
    public String display() {
        return calculator.getDisplayValue();        
    }     
    
    public void button(String buttons) {
        for (char button : buttons.toCharArray()) {
            pressButton(button);
        }
    }

    private void pressButton(char button) {
        switch (button) {
            case '0':calculator.press0(); break;
            case '1':calculator.press1(); break;
            case '2':calculator.press2(); break;
            case '3':calculator.press3(); break;
            case '4':calculator.press4(); break;
            case '5':calculator.press5(); break;
            case '6':calculator.press6(); break;
            case '7':calculator.press7(); break;
            case '8':calculator.press8(); break;
            case '9':calculator.press9(); break;
            case '+':calculator.pressPlus(); break;
            case '-':calculator.pressMinus(); break;
            case '=':calculator.pressResult(); break;
            case 'C':calculator.pressClear(); break;
            case '.':calculator.pressDot(); break;
            default: throw new IllegalArgumentException("Unexpected command " + button);
        }        
    }    
}

А вот, что в результате у нас получилось.

Ой! Кажется заказчик ошибся, создавая таблички :)


Исправим эту неточность в документе-тесте и пойдем дальше.

У нас осталась еще две таблички-фикстуры, которые пока не рабочие


Отремонтируем их! Тут нам fit.ActionFixture не особо поможет. Зато у нас остались еще два базовых класса, один из которых нам наверняка поможет - это fit.ColumnFixture.

Для этого две наши таблички

|Операнд 1|Операнд 2|Результат|
|123|456|579|
|45|1|46|
|0|1|1|
|10|1200|1210|
|555|777|1132|

|Операнд 1|Операнд 2|Результат|
|222|111|111|
|333|3|330|
|0|1|-1|
|23|25|-2|
|57|57|0|

модифицируем так
 
!|calculator.fixtures.CalculatorOperationFixture|
|operand1|operand2|plus()|
|123|456|579|
|45|1|46|
|0|1|1|
|10|1200|1210|
|555|777|1132|

!|calculator.fixtures.CalculatorOperationFixture|
|operand1|operand2|minus()|
|222|111|111|
|333|3|330|
|0|1|-1|
|23|25|-2|
|57|57|0|

Так как класс фикстуры пустой

package calculator.fixtures;

import fit.Fixture;

public class CalculatorOperationFixture extends Fixture {
}

то таблички после запуска документа-теста выглядят так (ignored)


чтобы таблички ожили, необходимо реализовать класс-фикстуру

package calculator.fixtures;

import fit.ColumnFixture;

public class CalculatorOperationFixture extends ColumnFixture {
    
    public String operand1;
    public String operand2;
    
    public String plus() {
        return "";
    }
    
    public String minus() {
        return "";    
    }
}

после чего таблички заговорят


Принцип работы фикстуры fit.ColumnFixture прост.

!|path.SomeMyColumnFixture|
|fieldname1|fieldname2|fieldname3|method1()|method2()|
|someData1|someData2|someData3|someData4|someData5|
|someData6|someData7|someData8|someData9|someData10|
|someData11|someData12|someData13|someData14|someData15|

Первая колонка указывает как будет происходить взаимодействие с классом path.SomeMyColumnFixture.

fieldname1, fieldname2, fieldname3 - public поля в которые последовательно будут вставляться данные из их колонок

method1, method2 - список методов результат выполнения которых (после вставки данных в поля) последовательно сравнится с данными из их колонок.

Сложно. Давай попроще - при выполнении этой фикстуры подряд будет сделано:
  1. fixture = new path.SomeMyColumnFixture();
  2. fixture.fieldname1 = "someData1"
  3. fixture.fieldname2 = "someData2"
  4. fixture.fieldname3 = "someData3"
  5. check that fixture.method1() == someData4
  6. check that fixture.method2() == someData5
  7. fixture.fieldname1 = "someData6"
  8. fixture.fieldname2 = "someData7"
  9. fixture.fieldname3 = "someData8"
  10. check that fixture.method1() == "someData9"
  11. check that fixture.method2() == "someData10"
  12. fixture.fieldname1 = "someData11"
  13. fixture.fieldname2 = "someData12"
  14. fixture.fieldname3 = "someData13"
  15. check that fixture.method1() == "someData14"
  16. check that fixture.method2() == "someData15"

Хух...

Теперь, чтобы все заработало нам надо подружить фикстуру с классом Calculator.

package calculator.fixtures;

import fit.ColumnFixture;

public class CalculatorOperationFixture extends ColumnFixture {
    
    private Calculator calculator = new Calculator();  
 
    public String operand1;
    public String operand2;
    
    public String plus() {
        return calculator.plus(operand1, operand2);
    }
    
    public String minus() {
     return calculator.minus(operand1, operand2);    
    }
}

и запустить тест


Упс! Опять ошибочка в требованиях. Ну уж этот заказчик! Спешит, блин!

После исправлений документа-теста мы добились заветной зеленой полосы.



На этом пока все. Исходный код калькулятора, фикстур и документ-тест в wiki коде можно взять тут.

Позже чуть расскажу, как пользоваться базовым классом fit.RowFixture для построения третьего типа фикстур. А так же разберем как в Fitnesse делаются так привычные нам из jUnit SetUp, TearDown и Suite.

Java for fun: Разрабатываем Fitnesse тесты #1

Этим постом начну новую серию цель которой помогать на начальном этапе. Я думаю раз в две недели каждый разработчик может написать нечто подобное, потому как приблизительно за это время у него набирается экспертизы достаточно, для того, чтобы позволить ускориться его коллегам. Так и со мной - около двух недель назад я получил конкретное задание - и теперь сейчас я могу написать этот пост про замечательную разработку Fitnesse. Вперед.

К чему (в частности) стремятся Agile проекты? Минимум документации в пользу самотестирующегося и самодокументирующегося кода. А все потому, что документацию надо поддерживать наряду с кодом. Итак, если есть тесты, которые тестируют систему, а еще они написаны понятным языком, то это хорошо. Но вместе с тем в больших проектах от документации не особо и уходят. Ну и неудивительно - документация описывает систему на достаточно абстрактном уровне (бизнеса), а тесты они все же сильно привязаны к интерфейсам модулей (понятным только программистам). Заказчику легче работать с тестами первого типа, да и вообще, с не с тестами, а с текстовыми документами, графиками, табличками. Код же для программистов. И возникает вопрос, как тут избавиться от поддержки двух версий описания системы (код и документ с таблицами)? АА выход придумали и называется он Fitnesse.

Рассмотрим жизненный цикл документа в команде, которая работает с заказчиком с помощью Fitnesse. Все начинается с того, что у заказчика загорается лампочка и он начинает описывать команде разработчиков, как должна работать будущая система. Вначале тексту дает, позже у программистов появляются наводящие вопросы и чаще всего графики, таблички, блок-схемы...

Давайте рассмотрим пример подобного требования на примере некого устройства.

Необходимо смоделировать устройство, позволяющее производить арифметические вычисления. Устройство состоит из кнопок «+» «–» «=», цифровой клавиатуры с кнопкой «.» и кнопки «с» (сброс). Нажатие на каждую кнопку считается операцией.

Сброс состояния устройства
 
Операция
Показания экрана после выполнения операции
C
0
1
1
C
0

Ввод числа в устройство

Операция
Показания экрана после выполнения операции
C
0
1
1
2
12
5
125
7
1257
8
12579

Ввод дробного числа

Операция
Показания экрана после выполнения операции
C
0
5
5
6
56
7
567
.
567.
1
567.1
2
567.12

Далее, в колонке «Операция» (для удобства) допустимо упоминание нескольких операций идущих подряд, например.

Операция
Показания экрана после выполнения операции
C123.567
123.567

что равносильно

Операция
Показания экрана после выполнения операции
C
0
1
1
2
12
3
123
.
123.
5
123.5
6
123.56
7
123.567

Операция суммирования
Суммирование - это операция, которая работает так

Операнд 1
Операнд 2
Результат
123
456
579
45
1
46
0
1
1
10
1200
1210
555
777
1132
 
Суммирование двух чисел

Операция
Показания экрана после выполнения операции
C123
123
+
123
456
456
=
579

Суммирование более двух чисел

Операция
Показания экрана после выполнения операции
C123
123
+
123
456
456
+
579
567
567
+
1146
111
111
=
1257

Операция вычитание
 Вычитание - это операция, которая работает так

Операнд 1
Операнд 2
Результат
222
111
111
333
3
330
0
1
-1
23
25
-2
57
57
0

Вычитание двух и более чисел работает аналогично суммированию

Операция
Показания экрана после выполнения операции
C123
123
-
123
456
456
=
-333


Операция
Показания экрана после выполнения операции
C123
123
123
456
456
-333
567
567
-900
111
111
=
-1011

Можно комбинировать суммирование и вычитание

Операция
Показания экрана после выполнения операции
C123
123
­–
123
456
456
+
-333
567
567
=
234

Отрицательное число в качестве первого операнда

Операция
Показания экрана после выполнения операции
C
0
­–123
123
+
-123
124
124
=
1
Вот такой такой вот документ может случиться в результате общения команды разработчиков с заказчиком.

После того, как документ есть у команды на руках - его анализируют на наличие сходных табличек. В данном документе есть два вида табличек - таблички первого типа определяют, как работать с устройством, а остальные - дают пояснения к операциям. Цель этого этапа - вывести и описать все таблички, которые встречаются в терминологии заказчика. Если таблички не существенно отличаются, то можно их обобщить. Если встречаются графики, то их стоит представить в виде таблиц.

После того, как на руках имеются описания таблиц - можно приступить к реализации так называемых фикстур. Что это такое - станет ясно дальше. А пока мы скачаем Fitnesse.



Архив распакуем в какую-то папку в корне (в принципе не важно) диска и осмотримся внутри


Нас интересует файл run.bat запускающий сервер. Создадим рядом еще один bat файл с таким содержимым.

run -p 8085

В результате запуска этого файла мы увидим консоль с текстом

D:\fitnesse>run -p 8085

D:\fitnesse>java -Xmx100M -jar fitnesse.jar -p 8085
FitNesse (v20090321) Started...
        port:              8085
        root page:         fitnesse.wiki.FileSystemPage at ./FitNesseRoot
        logger:            none
        authenticator:     fitnesse.authentication.PromiscuousAuthenticator
        html page factory: fitnesse.html.HtmlPageFactory
        page version expiration set to 14 days.

Это значит - все прошло успешно и мы можем запустить в своем браузере localhost:8085


По этому сайту можно походить, а когда надоест то в адресной строке браузера введи http://localhost:8085/CalculatorTest
Т.к. это википодобная система, то тебе предложат создать новую страничку, внутренность которой наполни содержимым документа заказчика. Строчку "!contents -R2 -g -p -f -h" моно удалить. Напомню так же, что таблицы в wiki коде строятся с помощью символа "|".


Вот код из документа, конвенртированный в wiki формат.

Необходимо смоделировать устройство, позволяющее производить арифметические вычисления. Устройство состоит из кнопок «+» «–» «=», цифровой клавиатуры с кнопкой «.» и кнопки «с» (сброс). Нажатие на каждую кнопку считается операцией. 

Сброс состояния устройства

|Операция|Показания экрана после выполнения операции|
|C|0|
|1|1|
|C|0|

Ввод числа в устройство

|Операция|Показания экрана после выполнения операции|
|C|0|
|1|1|
|2|12|
|5|125|
|7|1257|
|8|12579|

Ввод дробного числа

|Операция|Показания экрана после выполнения операции|
|C|0|
|5|5|
|6|56|
|7|567|
|.|567.|
|1|567.1|
|2|567.12|

Далее, в колонке «Операция» (для удобства) допустимо упоминание нескольких операций идущих подряд, например.

|Операция|Показания экрана после выполнения операции|
|с123.567|123.567|

что равносильно

|Операция|Показания экрана после выполнения операции|
|C|0|
|1|1|
|2|12|
|3|123|
|.|123.|
|5|123.5|
|6|123.56|
|7|123.567|

Операция суммирования
Суммирование - это операция, которая работает так

|Операнд 1|Операнд 2|Результат|
|123|456|579|
|45|1|46|
|0|1|1|
|10|1200|1210|
|555|777|1132|

Суммирование двух чисел

|Операция|Показания экрана после выполнения операции|
|C123|123|
|+|123|
|456|456|
|=|579|

Суммирование более двух чисел

|Операция|Показания экрана после выполнения операции|
|C123|123|
|+|123|
|456|456|
|+|579|
|567|567|
|+|1146|
|111|111|
|=|1257|

Операция вычитание
Вычитание - это операция, которая работает так

|Операнд 1|Операнд 2|Результат|
|222|111|111|
|333|3|330|
|0|1|-1|
|23|25|-2|
|57|57|0|

Вычитание двух и более чисел 
Работает аналогично суммированию

|Операция|Показания экрана после выполнения операции|
|C123|123|
|-|123|
|456|456|
|=|-333|

|Операция|Показания экрана после выполнения операции|
|C123|123|
|–|123|
|456|456|
|–|-333|
|567|567|
|–|-900|
|111|111|
|=|-1011|

Комбинирование операций

|Операция|Показания экрана после выполнения операции|
|C123|123|
|–|123|
|456|456|
|+|-333|
|567|567|
|=|234|

Отрицательное число в качестве первого операнда

|Операция|Показания экрана после выполнения операции|
|C|0|
|–123|123|
|+|-123|
|124|124|
|=|1|

А вот как выглядит это все после нажатия кнопки Save. Впрочем, ничего необычного - это делает wiki.


Документ как документ, но нажмем кнопку Test на панели слева.


Что случилось? А случилось вот что - мы попробовали запустить тест, описанный в этом документе. Как такое возможно? А просто - каждая табличка представляет собой проверку и называется fixture (фикстура). Как мы определили раньше - в нашем документе-тесте два вида таблиц-фикстур. Пока фикстуры наши не совсем рабочие, о чем свидетельствует окраска таблиц в желтый цвет с вклиненным сообщением "Could not find fixture: Операция." Чтобы таблички-фикстуры заработали - надо описать java класс, обрабатывающий данные описанные в табличке-фикстуре документа-теста. Тут как раз и начинается третий этап обработки требований заказчика командой, работающей с использованием Fitnesse.

Создадим новый java проект в вашей любимой IDE (я юзаю Eclipse). К этому времени у тебя должна стоять виртуальная машина java и эта самая IDE.


Приличия ради я создам пакет "calculator.fixtures", в котором будут размещены все обе фикстуры.


Далее создадим папочку lib и скопируем туда библиотеку fitnesse.jar из папки с установленным fitnesse.


Эту библиотеку добавим к проекту.


Дальше создадим таких два класса

package calculator.fixtures;

import fit.Fixture;

public class CalculatorActionFixture extends Fixture {
 
}

package calculator.fixtures;

import fit.Fixture;

public class CalculatorOperationFixture extends Fixture {
 
}

Вернемся к шанему документу-тесту, нажмем кнопку "Edit" и вставим первой строчкой путь к папке с компилированными java классами проекта.


У меня это будет строчка "!path I:\MyFirstFitnesseFixtures\bin"


После сохранения в доке-тесте можно будет увидеть


Кстати так же можно указать и ссылку на jar файл, содержащий фикстуры к примеру так "!path I:\MyFirstFitnesseFixtures\calculator.jar", но в разработке я решил пропустить этап сборки в jar файл по причине экономии времени.

Дальше нам следует уточнить наши таблички-фикстуры, чтобы они ссылались каждая на свой java класс. Вот wiki код двух разных табличек после модицикации.

!|calculator.fixtures.CalculatorActionFixture|
|Операция|Показания экрана после выполнения операции|
|C|0|
|1|1|
|C|0|

!|calculator.fixtures.CalculatorActionFixture|
|Операция|Показания экрана после выполнения операции|
|C|0|
|1|1|
|2|12|
|5|125|
|7|1257|
|8|12579|

Как ты заметил, мы всего лишь добавили имя класса и пакет, в котором он размещен перед каждой таблицей. Формат строки
!|path.ClassName|
Вот то, что мы увидим в результате сохранения


Если теперь нажмем на test, то увидим как таблички посерели.


Это значит, что фикстура отработана, но данные таблицы проигнорены. Еще бы - классы фикстур у нас пустые совсем...

Базовый класс fit.Fixture содержит некоторое число методов, которые можно переопределить в наследниках. Сейчас нас интересует метод doRow а так же конструктор. Переопределим их.

package calculator.fixtures;

import fit.Fixture;
import fit.Parse;

public class CalculatorActionFixture extends Fixture {
 
    public CalculatorActionFixture() {
        super();
    }
 
    @Override
    public void doRow(Parse row) {
        super.doRow(row);
    } 
}

Поставим внутри методов по brеakpoint.

Теперь нам надо сделать возможным debug фикстур. Делается это так. Так же, как мы прописывали где находятся java class файлы пропишем строчку
!define COMMAND_PATTERN {java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=1044 -cp %p %m}


дальше настроим remote application debug в IDE, но пока запускать не будем


Теперь нажмем кнопку "Test" на документе-тесте


На этот раз выполнение тестов остановится в самой начальной фазе и не завершится, пока мы не подключим debugger IDE


После подключения IDE остановится в нашем breakpint


Поиграся немного.

Можно заметить, что конуструктор вызывается непосредственно перед каждой строкой
!|calculator.fixtures.CalculatorActionFixture|
А метод doRow для каждой последующей строчки таблицы.

Это FIT чудит. FIT - это фреймворк, который парсит HTML страничку, полученную из вики кода и встречая там табличку находит в сответствиее ей java класс наследник Fixture, который и запускает специальным образом - по одному экземпляру на одну таблицу, по одному методу doRow на <tr>...</td> пару тегов.

Как я говорил раньше есть и другие методы базового класса Fixture, которые можно переопределить. К примеру, метод doCell выполняется для каждой пары <td>...</td> в пределах одной row. Вот его сигнатура
public void doCell(Parse cell, int columnNumber) {
    super.doCell(cell, columnNumber);
}
А вот список всех остльных методов, как по мне - поле для экспериментов!


Если заглянуть вовнутрь параметра row метода doRow то можно увидеть его непростую структуру


Но на самом деле все просто - для таблички


Полезные значения будут такими:

первая колонка
row.parts.body = 'Операция'
row.parts.more.body = 'Показания экрана после выполнения операции'

следующая колонка 
row.more.parts.body = 'C'
row.more.parts.more.body = '0'

следующая колонка 
row.more.more.parts.body = '1'
row.more.more.parts.more.body = '1'

следующая колонка 
row.more.more.more.parts.body = '2'
row.more.more.more.parts.more.body = '12'

и так далее. Самое неприятное в написании фикстуры с нуля (а можно воспользоваться одним из подготовленных наследников Fixture, но об этом позже) - это то, что придется барахтаться среди этих more.more.more... Но ничего!

А вот пример того, как получается обратная связь

@Override
    public void doRow(Parse row) {
        Parse cell = row.parts;
        for (int i = 0; cell != null; i++, cell = cell.more) {   
            if (i%2 == 0) {
                cell.addToTag(" class=\"pass\"");       
                cell.addToBody(Fixture.label("OK"));
            } else {
                cell.addToTag(" class=\"fail\"");       
                cell.addToBody(Fixture.label("ERR"));
            }
        }
        super.doRow(row);
    }

и вот что в результате запуска теста случится с табличкой


Что из этого эксперимента можно было извлечь? Да то, что при запуске тестов фикстура не только отрабатывает каждую табличку, но и служит генератором таблички, которая отрисуется на место исходной.

На этом низкуровневое программирование заканчиваем. Дальше я расскажу про набор классов-наследников Fixture, которые позволяют отвлечься от row.more.more.more.parts.more.body...

Но это в следующий раз...