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


Интересна 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.

4 комментария:

  1. Анонимный5 июля 2011 г., 11:59

    Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  2. Анонимный5 июля 2011 г., 12:06

    Мне остается непонятным как FitNess связывается с тестированым десктопным/веб приложением. Нужен ли исходный код тестированого приложения для его тестирования? Буду благодарен за ответ на, возможно, глупый вопрос :)

    ОтветитьУдалить
  3. Вопрос очень даже не глупый. 15 минут вебсерфинга показал, что все зависит от того, какое именно приложение вы собрались тестировать.

    Вот например для тестирования Swing приложения тулза UISpec4J http://www.uispec4j.org/ - читал, что есть возможность ее подружить с Fitnesse.

    Вот еще один посрденик Ranorex - http://www.ranorex.com/blog/manage-test-data-and-execute-automtated-test-scenarios-with-fitnesse-and-ranorex

    А вот Abbot который поможет протестить ваше javaUI http://abbot.sourceforge.net/doc/overview.shtml а вот как его подружить с Fitnesse http://www.csis.pace.edu/~bergin/xp/guitesting.html

    В общем идея проста -> определяете то, что вы хотите протестить (что за приложение, есть или нет у него исходники) -> находите тулзу, которая специализируется на тестирование данного типа приложения -> находите способ, как ее подружить с fitnesse -> дружите

    ОтветитьУдалить
  4. Подскажите, пожалуйста, как привязать внешнюю библиотеку к Fitesse. Пытаюсь подружить Fitnesse с Selenium. Если подключать просто библиотек, фитнесс не видит селениумовские классы

    ОтветитьУдалить