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

Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 
Показаны сообщения с ярлыком PHP. Показать все сообщения
Показаны сообщения с ярлыком PHP. Показать все сообщения

среда, 24 апреля 2013 г.

Блочим Тролля

Тролля не корми - все в один голос говорят в сети. Кто-то же предлагает решения. Решил воспользоваться ими.

Итак самое интересное определение нашул тут.
Троллю совершенно безразлично, о чём идёт разговор. Ему нечего сказать по существу, он не способен сообщить ни о чём интересном. Его интересует только внимание к его персоне — и он будет делать что угодно, лишь бы обратить на себя внимание. Если вы ответите троллю, он полностью проигнорирует содержание вашего письма и напишет либо очередную глупость в ответ, либо просто вас обхамит. Если вы ему не ответите, он будет хамить ещё интенсивнее, пытаясь спровоцировать вас на грубость или необдуманное высказывание. Типичнейший приём тролля: если вы решили прекратить с ним разговор, он объявляет, что вы его испугались или что вам нечего ответить. Иногда обиженный отсутствием внимания тролль начинает систематически засорять форум мусором («флудить»). 
А вот решение, которое работает сейчас на моем блог. Идея гениальна свой простотой - надо всего лишь на фришный php хостинг разместить php файл, который будет принимать к себе всех посетителей, выкусывать их IP среди прочей информации и сравнивать с бан-списком. В случае, если юзер там найдется - php напечатает джаваскриптовый редирект, на страничку error.html. Редирект отработает в head блоке странички блога и как результат браузер отрисует эту страничку, а там будет написано в терминах blogspot - извините, но соответствующей странички нет!

Как узнать свой IP (чтобы поэкспериментировать) можно тут.

Хостинг бесплатный нашел тут.

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

Может кому-то пригодится.

понедельник, 27 августа 2012 г.

TDD на PHP используя Zend Studio - выделяем из калькулятора зависимость и мочим ее

Краткое содержание прошлых серий:
- подружили Zend Studio и Zend Server
- подружили Zend Studio и GitHub
- написали калькулятор по TDD

Калькулятор умеет суммировать числа в разных системах счисления от 2-ричной до 17-ричной. Его можно развивать дальше (умножение, деление, работа с отрицательными числами), но это не так интересно и будет выглядить +/- так же как и в прошлой серии. Сейчас же пойдем другим путем - от заказчика пришло требование-нежданчик. На базе существующего калькулятора необходимо реализовать калькулятор, работающий с римскими числами, при этом старая возможность суммировать в разных системах счисления должна оставаться не тронутой.

Как быть? Вариантов реализаций множество, но как показывает практика ту, которую мы разберем тут называют в самую последнюю очередь (или не называют вообще).

Если присмотреться, то в существубщем калькуляторе уже наблюдается нарушение принципа единой ответственности (SRP или Single Responsibility Principle)

class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;

        if ($this->isContainsInvalidNumber($expression)) {
            throw new RuntimeException('Invalid number');
        }
        
        if ($base > strlen($this->Digits) || $base <= 1) {
            throw new RuntimeException('Invalid base');
        }
        
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
            
        $sum = $this->hexToInt($out[0][0]) + 
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function isContainsInvalidNumber($expression) {
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
            
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;                
        }            
        return $is_invalid;        
    }
    
    private function intToHex($int) {
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
        
        return $result;
    }

    
    private function toHex($int) {
        return $this->Digits[$int];
    }
    
    private function hexToInt($hex) {        
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;

    }
    
    private function toInt($hex) {
        if (is_numeric($hex)) return $hex; 
        return strpos($this->Digits, $hex);
    }

}

Сейчас можно углядеть что он работает так - получая на вход два числа, валидирует правильность чисел, после конвертирует их в 10ричную систему, после складывает и конфертирует обратно в исходную систему счисления. конвертирует -> валидирует -> суммирует -> конвертирует. Не знаю как у тебя, но со словом калькулятор у меня больше ассоциируется глагол "суммирует", а вот конвертация и валидация - это что-то несвейственное ему. Кроме того именно конвертация+валидация подвергается обстрелу с новым требованием-нежданчиком от любимого заказчика. А раз уж меняется - инкапсулируй! Это один из ООП-шных советов. Давай проследуем ему.

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

Первым делом я хочу выделить логику конвертации из x-ричной системы счисления в 10-ричную.

[тест]
class ConvertorTest extends PHPUnit_Framework_TestCase {
  
    private $Convertor;
 

    protected function setUp() {
        parent::setUp ();
        $this->Convertor = new Convertor();
    }
 
    protected function tearDown() {
        $this->Convertor = null;
        parent::tearDown ();
    }
 
    public function testShouldConvertHexToInt() { // вот самое интересное
        $actual =  $this->Convertor->decode('1ABC', '16');
        $this->assertEquals(6844, $actual);
    }
  
}
[фикс]
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) { // все строки кроме этого метода были перенесены копипастом
        $this->Base = $base;
        
        return $this->hexToInt($hex);
    }
    
    private function hexToInt($hex) {
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;
    
    }
    
    private function toInt($hex) {
        if (is_numeric($hex)) return $hex;
        return strpos($this->Digits, $hex);
    }
}
[коммит] Начал выносить зависимости по конвертации в отдельный класс
[тест]
    public function testShouldConvertIntToHex() {
        $actual =  $this->Convertor->code('6844', '16');
        $this->assertEquals('1ABC', $actual);
    }
[фикс]
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) {
        $this->Base = $base;
        
        return $this->hexToInt($hex);
    }
    
    public function code($hex, $base) { // добавили новый метод
        $this->Base = $base;
    
        return $this->intToHex($hex);
    }
    
    private function intToHex($int) { // это скопитырили из калькулятора
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
    
        return $result;
    }
    
    private function toHex($int) {  // это тоже скопитырили из калькулятора
        return $this->Digits[$int];
    }
    
   ... 
}
[коммит] Вынес вторую, операцию кодирование в x-ричную систему Теперь сразу два теста, один положительный а другой отрицательный.
[тест]
    public function testShouldValidateTrueIfValid() {
        $this->assertTrue($this->Convertor->isValid('6844', '16'));
    }
    
    public function testShouldValidateFalseIfInvalid() {
        $this->assertFalse($this->Convertor->isValid('1ABG', '16'));
    }
[фикс]
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) {
        $this->Base = $base;
        
        return $this->hexToInt($hex);
    }
    
    public function code($hex, $base) {
        $this->Base = $base;
    
        return $this->intToHex($hex);
    }
    
    public function isValid($hex, $base) { // новый метод
        $this->Base = $base;
    
        return !$this->isContainsInvalidNumber($hex);
    }
    
    ... 
    
    private function isContainsInvalidNumber($expression) { // копитыренный метод
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
                
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;
        }
        return $is_invalid;
    }
}

?>
[коммит] Вынес вторую, операцию кодирование в x-ричную систему Я вижу много дублирования, а потому сделаю рефакторинг
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function decode($hex, $base) { // тут передается $base
        $this->Base = $base; // а тут устанавливается в поле
        
        return $this->hexToInt($hex);
    }
    
    public function code($hex, $base) { // и тут
        $this->Base = $base; // и тут
    
        return $this->intToHex($hex);
    }
    
    public function isValid($hex, $base) { // и тут
        $this->Base = $base; // и тут
    
        return !$this->isContainsInvalidNumber($hex);
    }
[рефакторинг] избавился от дублирования
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function __construct($base) { // появился конструктор принимающий основание
        $this->Base = $base;
    }
    
    public function decode($hex) { // зато основание пропало отсюда
        return $this->hexToInt($hex);
    }
    
    public function code($hex) {
        return $this->intToHex($hex);
    }
    
    public function isValid($hex) {
        return !$this->isContainsInvalidNumber($hex);
    }
Естественно это привело к исправлению тестов.
class ConvertorTest extends PHPUnit_Framework_TestCase {
        
    private $Convertor;
    
    protected function setUp() {
        parent::setUp ();
        $this->Convertor = new Convertor(16); // тут
    
    }
    
    protected function tearDown() {
        $this->Convertor = null;
        parent::tearDown ();
    }
    
    public function testShouldConvertHexToInt() {
        $actual =  $this->Convertor->decode('1ABC'); // тут
        $this->assertEquals(6844, $actual);
    }
    
    public function testShouldConvertIntToHex() {
        $actual =  $this->Convertor->code('6844'); // тут
        $this->assertEquals('1ABC', $actual);
    }
    
    public function testShouldValidateTrueIfValid() {
        $this->assertTrue($this->Convertor->isValid('6844')); // тут
    }
    
    public function testShouldValidateFalseIfInvalid() {
        $this->assertFalse($this->Convertor->isValid('1ABG')); // а тут даже тест немного переделал
    }
    
}
[коммит]
[рефакторинг] Дальше пачка методов ничего не делающих, а только валидирующих
    public function decode($hex) {
        return $this->hexToInt($hex); 
    }
    
    public function code($hex) {
        return $this->intToHex($hex);
    }
    
    public function isValid($hex) {
        return !$this->isContainsInvalidNumber($hex);
    }
Вердикт - убрать, переименовав hexToInt в decode, intToHex в code, isContainsInvalidNumber в isValid с инвертированием return!
class Convertor {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function __construct($base) {
        $this->Base = $base;
    }
    
    public function code($int) { // переименовали и сделали public
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
    
        return $result;
    }
    
    
    private function toHex($int) {
        return $this->Digits[$int];
    }
    
    public function decode($hex) { // переименовали и сделали public
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;
    
    }
    
    private function toInt($hex) {
        if (is_numeric($hex)) return $hex;
        return strpos($this->Digits, $hex);
    }
    
    public function isValid($expression) { // переименовали и сделали public
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
                
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;
        }
        return !$is_invalid; // инвертировали результат перед возвратом
    }
}
[коммит]
[рефакторинг]Теперь можно подключить акуратно перенесенную реализацию к калькулятору и удалить у него лишние методы.
require_once 'application\models\Convertor.php'; // незабываем импорт

class Calculator {    
    
    // это нам больше тут не понадобится - удаляем
    // private $Base; 
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $convertor = new Convertor($base); // создаем новый инстанс конвертера

        if (!$convertor->isValid($expression)) { // валидация (осторожно, инвертируем результат)
            throw new RuntimeException('Invalid number');
        }
        
        if ($base > strlen($this->Digits) || $base <= 1) {
            throw new RuntimeException('Invalid base');
        }
        
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
            
        $sum = $convertor->decode($out[0][0]) + // конвертер декодирует, а калькулятор суммирует
               $convertor->decode($out[0][1]);    

        return $convertor->code($sum); // конвертер кодирует обратно
    }

}
[коммит]

Что мы имеем сейчас? Зависимость акуратно шаг за шагом была вынесена в отдельный класс Convertor.

Старые тесты калькулятора тестируют два класса вместе, что не совсем юнит тест. Есть зависимость у калькулятора в виде инстанциирования new Convertor в теле метода calculate. Эту зависимость необходимо разорвать.

Часть тестов из текущего интеграционного (т.к. тестирует два класса в связке) перенести в ConvertorTest, а CalculatorTest переписать на моках.

Успокоения ради можно оставить один интеграционный тест, который проверит два класса в связке.

Но прежде всего хотелось бы сделать метод isValid не знающим про знак суммирования.

Так же стоит еще проверку на base вынести в конструктор Converter - ей там больше место.

После этой операции можно приступать к разработке сумматора римских чисел.

Это мой тест лист на текущий момент. Из него выбираем следующий наиболее простой шаг и вперед!

[рефакторинг] Начну с того, чтобы isValid не знал про суммирование, а валидация основания проходила в конструкторе конвертора.

class Calculator {    
    
//     private $Digits = "0123456789ABCDEFG"; // это лишнее теперь
    
    public function calculate($expression, $base) {
        $convertor = new Convertor($base);

//         if (!$convertor->isValid($expression)) {   // валидацию разместили ниже, после парсинга
//             throw new RuntimeException('Invalid number');
//         }
        
//         if ($base > strlen($this->Digits) || $base <= 1) { // это отнесли в конструктор конвертора
//             throw new RuntimeException('Invalid base');
//         }
        
        preg_match_all('/[0-9A-Z]+/', $expression, $out); // поменяли регекспу - теперь она выкусывает все символьно-цифровое
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
        
        if (!$convertor->isValid($out[0][0]) || !$convertor->isValid($out[0][1])) { // добавили новую проверку для операндов
            throw new RuntimeException('Invalid number');
        }
            
        $sum = $convertor->decode($out[0][0]) + 
               $convertor->decode($out[0][1]);    

        return $convertor->code($sum);
    }

}

class Convertor {  // а теперь класс конвертора
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function __construct($base) {
        if ($base > strlen($this->Digits) || $base <= 1) { // добавили проверку
            throw new RuntimeException('Invalid base');
        }
        
        $this->Base = $base;
    }
     
        public function isValid($expression) {
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
//             if ($expression[$index] == '+') { // это лишнее уже
//                 continue;
//             }
                
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;
        }
        return !$is_invalid;
    }

    ...
[коммит]
[рефакторинг] Сейчас я бы перенес часть тестов на их законное место. Так как класс калькулятора теперь уже занимается только суммированием, то часто тестов из CalculatorTest должна быть перенесена в ConvertorTest. Данные для тестов конвертора будем брать из рассчета того, что передается в него сейчас при запуске тестов CalculatorTest.

class ConvertorTest extends PHPUnit_Framework_TestCase {
        
    public static function validDataProvider() {
        return array(
                array(10, '2', '2'),
                array(10, '4', '4'),
                array(10, '3', '3'),
                array(10, '7', '7'),
                array(10, '11', '11'),
                array(10, '22', '22'),
                array(10, '33', '33'),
                
                array(16, '1', '1'),
                array(16, '2', '2'),
                array(16, '5', '5'),
                array(16, '6', '6'),
                array(16, '8', '8'),
                array(16, '9', '9'),
                array(16, 'A', '10'),
                array(16, 'B', '11'),
                array(16, 'C', '12'),
                array(16, 'D', '13'),
                array(16, 'E', '14'),
                array(16, 'F', '15'),
                array(16, '11', '17'),
                array(16, '19', '25'),
                array(16, '1B', '27'),
                array(16, '1E', '30'),
                array(16, '1C', '28'),
                array(16, '1D', '29'),
                array(16, '22', '34'),
                array(16, '44', '68'),
                array(16, '99', '153'),
                array(16, '100', '256'),
                array(16, '101', '257'),
                array(16, '132', '306'),
                
                array(17, '2', '2'),
                array(17, '3', '3'),
                array(17, '4', '4'),
                array(17, 'D', '13'),
                array(17, 'F', '15'),
                array(17, 'G', '16'),
                array(17, '10', '17'),
                array(17, '1G', '33'),
                array(17, '1F', '32'),
                array(17, '3F', '66'),
                
                array(2, '0101010101', '341'),
                array(2, '1010101010', '682'),
                array(2, '1111111111', '1023'),
        );
    }
    
    public static function invalidDataProvider() {
        return array(
            array(16, 'G'),
            array(2, '10300'),
            array(4, '1A1'),
            array(16, 'QWE'),
            array(16, 'ASD'),    
        );
    }
    
    /**
     * @dataProvider validDataProvider
     */
    public function testShouldConvertHexToInt($base, $hex, $int) {
        $convertor = new Convertor($base);
        $actual =  $convertor->decode($hex);
        $this->assertEquals($int, $actual);
    }
    
    /**
     * @dataProvider validDataProvider
     */
    public function testShouldConvertIntToHex($base, $hex, $int) {
        $convertor = new Convertor($base);
        $actual =  $convertor->code($int);
        $this->assertEquals($hex, $actual);
    }
    
    /**
     * @dataProvider validDataProvider
     */
    public function testShouldValidateTrueIfValid($base, $hex) {
        $this->validate($base, $hex, true);
    }
    
    /**
     * @dataProvider invalidDataProvider
     */
    public function testShouldValidateFalseIfInvalid($base, $hex) {
        $this->validate($base, $hex, false);
    }
    
    private function validate($base, $hex, $isValid) {
        $convertor = new Convertor($base);
        $actual =  $convertor->isValid($hex);
        $this->assertEquals($isValid, $actual);
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsMoreThan17() {
        new Convertor(18);
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsLessThan2() {
        new Convertor(1);
    }
}
а вот что остается от стартого CalculatorTest
class CalculatorUnitTest extends PHPUnit_Framework_TestCase {
        
    private $Calculator;
    
    protected function setUp() {
        parent::setUp ();
        $this->Calculator = new Calculator();
    
    }
    
    protected function tearDown() {
        $this->Calculator = null;
        parent::tearDown ();
    }
    
    public function testShouldSumWhenXPlusY() {
        $this->assertEquals(444, $this->Calculator->calculate('123+321', 10));
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenInvalidExpression() {
        $this->Calculator->calculate('1', '10');
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenMoreThanOnePlus() {
        $this->Calculator->calculate('1++3', '10');
    }
        
}
[коммит]
 
Я его назвал CalculatorUnitTest, CalculatorTest оставил как есть (для сравнения, на реально проекте я бы удалил интеграционный). Мне в юнит тест для калькулятора предстоит еще добавить несколько тестов, чтобы покрытие калькулятора было максимальным. Тесты не на состояние а на поведение - мне интересно как общается калькулятор с конвертером, и тут без моков нам не обойтись....

Давай сделаем шажок немножечко побольше (рискнем) и провернем сразу несколько операций. Чтобы продемонстрировать что изменилось, покажу diff с помощью тортилки

1) выделим из Calculator зависимость от Converter (будем ее через конструктор инджектить). А так как $base - свойство Converter то из метода calculate так же убираем $base. 


Это приведет к тому, что нам надо будет исправить интеграционный тест (Calculator + Convertor). Напомню его я держу для сравнения, в реальном мире я бы его удалил где-то сейчас (или чуть раньше).


Это безобразно - снова купа дублирования, потому как сетап уже нам не поможет ($base у разных тестов разный и теперь он задается в конструкторе). Но еще можно заметить, что в некоторых тестах мы явно тестируем Convetror и не место тут этим тестам - в общем как ни крути интеграционный тест чем дальше тем больше путает...


2) А что касается юнит теста, то тут все более менее красиво. Настроили в сетапе мок, 

 Сконфигурировали в тесте моки с помощью методов should...... (я повуделял их для лучшей читабельности теста). И усе!
[коммит] 





Пару линков:
Хорошая статья по мокам.
И репозиторий, если удруг понадобится.


Дальше сделаем сумматор римских чисел. Тут чисстое ТДД, нас на секундочку больше не интересует калькулятор - мы просто переименуем Convertor на HexConvertor, и начнем разрабатывать новый класс RymConvertor. Но это уже в следующий раз.... 

четверг, 26 июля 2012 г.

TDD на PHP используя Zend Studio - собственно, калькулятор

Краткое содержание прошлых серий:
- подружили Zend Studio и Zend Server
- подружили Zend Studio и GitHub

Дальше у нас чистой воды TDD. Буду выкладывать код по очереди [тест], [фикс] [тест][фикс]. Всю хистори можно посмотреть тут (файлы Calculator.php и CalculatorTest.php).

[тест]
    public function testShould2When2Plus4() {
        $calculator = new Calculator();
        $actual =  $calculator->calculate('2+2');
        $this->assertEquals('4', $actual);
    }
[фикс]
class Calculator {
    public function calculate($expression) {
        return 4; // это самое простое, что заставит калькулятор работать!
    }
}
[коммит] 2+2 =4 !!!
 [тест]
    public function testShould3When4Plus7() {
        $calculator = new Calculator();
        $actual =  $calculator->calculate('3+4');
        $this->assertEquals('7', $actual);
    }
[фикс]
class Calculator {
    public function calculate($expression) {
        return substr($expression, 0, 1) + substr($expression, 1, 2); // теперь односимвольные числа умеем суммировать 
    }
}
[коммит] Теперь калькулятор может суммировать два однозначных числа

[рефакторинг] Выделил setup в тестах
class CalculatorTest extends PHPUnit_Framework_TestCase {

    private $Calculator; // поле :)                       

    protected function setUp() { // сетап вызовется перед каждым тестом                  
        parent::setUp ();                          
        $this->Calculator = new Calculator(); // и создаст калькулятор
    }                                              

    protected function tearDown() { // а это чудо восле каждого теста                 
        $this->Calculator = null; // обнулит ссылку                 
        parent::tearDown ();                       
    }                                              

    public function testShould4When2Plus2() {
        $actual = $this->Calculator->calculate('2+2'); // теперь можно пользоваться полем напрямую
        $this->assertEquals('4', $actual);
    }     

    public function testShould7When3Plus4() {
        $actual = $this->Calculator->calculate('3+4');
        $this->assertEquals('7', $actual);
    }

}
[коммит]

[тест]
    public function testShould33When11Plus22() { // новый тест
        $actual = $this->Calculator->calculate('11+22');
        $this->assertEquals('33', $actual);
    }
[фикс]
class Calculator {     

    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);  // усложняем логику
        $sum = (int)($out[0][0]) + (int)($out[0][1]);        

        return $sum;
    }
}
[коммит] Теперь калькулятор умеет суммировать многозначные числа

[рефакторинг] Выделил data provider в тестах
class CalculatorTest extends PHPUnit_Framework_TestCase {

    ...

    public static function provider() { // поставщик данных теста экономит место
        return array(
            array(2, 2, 4),
            array(3, 4, 7),
            array(11, 22, 33),
        );
    }
    
    // эта аннотация важна
    /**
     * @dataProvider provider 
     */
    public function testShouldSumWhenXPlusY($x, $y, $expected) { // тест-шаблон наполнится данными
        $actual = $this->Calculator->calculate($x.'+'.$y);
        $this->assertEquals($expected, $actual);
    }
}
[коммит]

[тест]
    // эта аннотация говорит, что ловим исключение
    /**
     * @expectedException RuntimeException
     */
    public function testShouldExceptionWhenInvalidExpression() {
        $this->Calculator->calculate('1');
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2) {  // вот и проверочка                             
            throw new RuntimeException('Invalid expression format'); 
        }                                                            

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        return $sum;
    }
[коммит] Есть простая валидация выражения
[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenMoreThanOnePlus() {
         $this->Calculator->calculate('1++3');
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) { // добавили подусловие
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        return $sum;
    }
[коммит] Валидция выражения на количество знаков + (должно быть 1)

[рефакторинг] Добавил новый параметр методу - $base - основание системы счисления
class CalculatorTest extends PHPUnit_Framework_TestCase {

   ...

    public static function provider() {
        return array(
            array(2, 2, 10, 4), // передаю во всех тестах, для 10ричной системы
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
        );
    }

    /**
     * @dataProvider provider
     */
    public function testShouldSumWhenXPlusY($x, $y, $base, $expected) { // расширил метод-шаблон
        $actual = $this->Calculator->calculate($x.'+'.$y, $base);
        $this->assertEquals($expected, $actual);
    }

    ...

}
[фикс]
class Calculator {     
        public function calculate($expression, $base) { // и добавил в сигнатуре метода
            ...
[коммит] Первый шаг в сторону 16-ричного калькулятора - введение основы системы счисления как параметр

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A),  // новый тест   
        );
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        if ($sum == 10 && $base == 16) { // добавили проверочку (ужас, но работает!)
            return "A";                  
        }                                

        return $sum;
    }
[коммит] В 16ричной 9 + 1 = А

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F), // еще тест    
        );
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        if ($base == 16) {
            if ($sum == 10) {
                return "A";
            } else if ($sum == 15) { // проверка усложнилась
                return "F";
            }
        }                                

        return $sum;
    }
[коммит] В 16ричной 6 + 9 = F

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E), // еще тест
        );
    }
[фикс]
class Calculator {

    private $Digits = "0123456789ABCDEF";  // выделили все буковки

    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        if ($base == 16) {
            if ($sum > 9) { // и тут сделали сразу для всех остальных буковок
                return $this->Digits[$sum];
            }
        }                               

        return $sum;
    }
}
[коммит] В 16ричной 6 + 8 = E

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), // еще тест  
        );
    }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          preg_match_all("/[0-9A]+/", $expression, $out); // тут добавили буковку
          
          if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
               throw new RuntimeException('Invalid expression format');
          }
               
          $sum = $this->toInt($out[0][0]) + // небольшой фикс 
                  (int)($out[0][1]);     

          if ($base == 16) {
               if ($sum > 9) {
                    return $this->Digits[$sum];
               } 
               
          }
          
          return $sum;
     }
     
     private function toInt($hex) { // новый метод
          if (is_numeric($hex)) return $hex; 
          if ($hex == "A") return 10;
     }
}
[коммит] В 16ричной A + 5 = F

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), 
            array(1, E, 16, F), // еще тест
        );
    }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          preg_match_all('/['.$this->Digits.']+/', $expression, $out); // сделали вообще универсальную регекспу
          
          if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
               throw new RuntimeException('Invalid expression format');
          }
               
          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]);  // тут исправили   

          if ($base == 16) {
               if ($sum > 9) {
                    return $this->Digits[$sum];
               } 
               
          }
          
          return $sum;
     }
     
     private function toInt($hex) {
          if (is_numeric($hex)) return $hex; 
          if ($hex == "A") return 10;
          if ($hex == "E") return 14; // тут добавили
     }
}
[коммит] В 16ричной 1 + E = F

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), 
            array(1, E, 16, F),  
            array(2, B, 16, D), // еще тест
        );
    }
[фикс]
     private function toInt($hex) {
          if (is_numeric($hex)) return $hex; 
          return strpos($this->Digits, $hex); // сделали универсальнее
     }
[коммит] В 16ричной 2 + B = D

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), 
            array(1, E, 16, F),  
            array(2, B, 16, D),
            array(B, E, 16, 19), // еще тест
        );
    }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          ...    

          if ($base == 16) {                              
               if ($sum > 16) { // добавили подифчик
                    $sum = 10 + $sum - 16;
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }
                
               
          }
          
          return $sum;
     }
     
     ...
}
[коммит] В 16ричной B + E = 19

[рефакторинг] Немного переделал логику
class Calculator {    
    
    private $Digits = "0123456789ABCDEF";
    
    public function calculate($expression, $base) {
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->toInt($out[0][0]) + 
               $this->toInt($out[0][1]);    

        if ($base == 16) {                        
            if ($sum > 16) {
                return '1'.($sum - 16); // тут
            } else if ($sum > 9) {
                return $this->Digits[$sum];
            }                     
        }
        
        return (string)$sum; // и тут
    }
    ...
[коммит]

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          ...     

          if ($base == 16) {                              
               if ($sum > 16) {
                    return '1'.$this->toHex($sum - 16); // исправили
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }                          
          }
          
          return (string)$sum;
     }
     
     private function toHex($int) { // новый метод
          if ($int == 11) return "B";
          return $int;
     }
     
     ...
}
[коммит] В 16ричной C + F = 1B

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'), // еще тест
          );
     }
[фикс]
     private function toHex($int) {
          return $this->Digits[$int]; // сделали универсальным
     }
[коммит] В 16ричной F + F = 1E

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Base; // теперь нам основание нужно в другом методе
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          $this->Base = $base; // потому сохраняем его в поле
          preg_match_all('/['.$this->Digits.']+/', $expression, $out);
          
          if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
               throw new RuntimeException('Invalid expression format');
          }
               
          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]);     

          if ($base == 16) {                              
               if ($sum > $base) { // тут чуть порефакторили
                    return '1'.$this->toHex($sum - 16);
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }                          
          }
                    
          return (string)$sum;
     }
     
     private function toHex($int) {
          return $this->Digits[$int];
     }
     
     private function toInt($hex) {          
          if (strlen($hex) == 2) { // и тутки добавили целый if
               return $this->Base*$this->toInt(substr($hex, 0, 1)) + $this->toInt(substr($hex, 1, 1));
          }
          
          if (is_numeric($hex)) return $hex; 
          return strpos($this->Digits, $hex);
     }
[коммит] В 16ричной 1C + 1 = 1D

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Base; 
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          ...    

          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]); 
         
          if ($base == 16) {                              
               if ($sum >= $base*2) { // добавили еще один if
                    return '2'.$this->toHex($sum - 32);
               } else if ($sum > $base) { // дальше без изменений
                    return '1'.$this->toHex($sum - 16);
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }                          
          }                                            
                    
          return (string)$sum;
     }
     
     ...
[коммит] В 16ричной 11 + 11 = 22

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Base;
     private $Digits = "0123456789ABCDEFG";
     
     public function calculate($expression, $base) {
          ...
               
          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]);     

          $big = (int)($sum / $base); // тут чуть поменяли логику
          if ($big <> 0) {
               return $this->toHex($big).$this->toHex($sum % $base);
          }
                    
          return $this->toHex($sum); // и тут :)
     }
     
     ...
}
[коммит] В 16ричной 22 + 22 = 44

[рефакторинг] Выделил новый метод из тела calculate
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->toInt($out[0][0]) + 
               $this->toInt($out[0][1]); ...    

        return $this->intToHex ($sum); // выделяли отсюда
    }

    private function intToHex($int) { // новый метод
        $big = (int)($int / $this->Base);
        if ($big <> 0) {
            return $this->toHex($big).$this->toHex($int % $this->Base);
        }
                
        return $this->toHex($int);
    }
    ... 
[коммит]

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'),
              array('99', '99', 16, '132'), // еще тест
          );
     }
[фикс]
     private function intToHex($int) {
          $big = (int)($int / $this->Base);
          if ($big <> 0) {
               return $this->intToHex($big).$this->intToHex($int % $this->Base); // тут сделали рекурсию
          }
                    
          return $this->toHex($int);
     }
[коммит] В 16ричной 99 + 99 = 132

[рефакторинг] Выделил еще один махонький метод и переименовал (Внимание!) toInt в hexToInt.
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->hexToInt($out[0][0]) + // переименовали
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function intToHex($int) { 
        $big = (int)($int / $this->Base);
        if ($big <> 0) {
            return $this->intToHex($big).$this->intToHex($int % $this->Base);
        }
                
        return $this->toHex($int);
    }

    
    private function toHex($int) { 
        return $this->Digits[$int];
    }
    
    private function hexToInt($hex) { // переименовали       
        if (strlen($hex) == 2) {
            return $this->Base*$this->hexToInt(substr($hex, 0, 1)) + $this->hexToInt(substr($hex, 1, 1));
        }
        
        return $this->toInt ($hex);

    }
    
    private function toInt($hex) { // выделили новый 
        if (is_numeric($hex)) return $hex; 
        return strpos($this->Digits, $hex);
    }

} 
[коммит]

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'),
              array('99', '99', 16, '132'),
              array('100', '1', 16, '101'), // еще тест
          );
     }
[фикс]
    private function hexToInt($hex) {  // полностью переписали      
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;

    }
[коммит] В 16ричной 100 + 1 = 101

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'),
              array('99', '99', 16, '132'),
              array('100', '1', 16, '101'),
              array('D', '3', 17, 'G'),  // добавили этот тест и все что ниже
              array('D', '2', 17, 'F'),
              array('D', '4', 17, '10'),
              array('G', '1', 17, '10'),
              array('G', 'G', 17, '1F'),
              array('1G', '1G', 17, '3F'),
          );
     }
[фикс]
class Calculator {    
    
    private $Digits = "0123456789ABCDEFG"; // фикс всего одна новая буковка
[коммит] Теперь калькулятор умеет считать и 17-ричные числа

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols2() { // этот тест на всякий для личного успокоения, каюсь :)
        $this->Calculator->calculate('10300+1', '2');
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols3() { // этот тест рабочий
        $this->Calculator->calculate('1+1A1', '4');
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
        
        if ($this->isInvalidNumber($out[0][0])) { // добавили новую проверку
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->hexToInt($out[0][0]) + 
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function isInvalidNumber($hex) { // и метод проверки
        $is_invalid = false;
        for ($index = 0; $index < strlen($hex); $index++) {
            $is_invalid |= $this->toInt($hex[$index]) >= $this->Base;                
        }            
        return $is_invalid;        
    }
    
   ...

}
[коммит] Валидация первого слагаемого, чтобы каждая цифра не выходила за пределы основания

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols() {
        $actual =  $this->Calculator->calculate('G+1', '16'); // буквы G быть не может
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
        
        if ($this->isInvalidNumber($out[0][0]) || $this->isInvalidNumber($out[0][1])) { // добавли еще одно условие
            throw new RuntimeException('Invalid expression format');
        }

        ...
[коммит] Валидация второго слагаемого чтобы не выходил за пределы основания

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols4() {
        $this->Calculator->calculate('QWE+ASD', '17');
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;

        if ($this->isContainsInvalidNumber($expression)) { // проверку перенесли чуть выше и для всего эксепешена сразу
            throw new RuntimeException('Invalid number');
        }
        
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
            
        $sum = $this->hexToInt($out[0][0]) + 
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function isContainsInvalidNumber($expression) { // метод переименовали и чуть переписали
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
            
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;                
        }            
        return $is_invalid;        
    }
    ...
[коммит] Теперь проводится валидация на всякие левые символы

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsMoreThan17() {
        $this->Calculator->calculate('1+1', '18');
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;

        if ($this->isContainsInvalidNumber($expression)) {
            throw new RuntimeException('Invalid number');
        }
        
        if ($base > strlen($this->Digits)) { // добавили еще один ифчик
            throw new RuntimeException('Invalid base');
        }
        ...
[коммит] Теперь и основание системы счисления валидаируется, не больше 17

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsLessThan2() {
        $this->Calculator->calculate('0+0', '1');
    }
[фикс]
        if ($base > strlen($this->Digits) || $base <= 1) { // добавили подусловие
            throw new RuntimeException('Invalid base');
        }
[коммит] Добавил проверку что основание не может быть меньше 2

[рефакторинг] немного упростил
    private function intToHex($int) {
        if ($int / $this->Base < 1) {
            return $this->toHex($int);
        }
                
        $high = (int)($int / $this->Base);
        return $this->intToHex($high).$this->toHex($int % $this->Base); // тут вызвал toHex, а то некошерно как-то intToHex для мелких чисел
    }
[коммит] Небольшой рефакторинг

[рефакторинг] избавился от рекурсии
    private function intToHex($int) {
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
        
        return $result;
    }
[коммит] Вот оно преимущество хорошего покрытия тестами - рефакторинг в удовольствие.

среда, 25 июля 2012 г.

TDD на PHP используя Zend Studio - настройка GitHub

В прошлывй раз мы подружили Zend Studio n Zend Server, а так же написали hello world ипоставли себе задачу написать калькулятор шеснадатеричных чисел. Вот тест


Первый фикс простой как двери



Дальше я хотел бы закоммититься но куда? GitHub :) немног ораньше надо было об этом думать, а потому я создам новый проект опять :)


Тут же IDE спросит меня установить ли плагин - да!


После установит все сама и перезапустится - ай лайкит ит!

Теперь у меня есть возможность выбрать новую вьюшку



Чето ругнулся по поводу HOME переменной, но я пока проигнорю


Вот она вьюшка, много кнопочек :)


Можно зайти в настройки проекта


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


 Теперь можно создать репозиторий

Тут я пошел не тем путем, но так как в процессе я не знал этого и делал скриншоты, то я их тут выложу, мало ли пригодятся. Можешь скипать все пометки [fail] аж до [OK]

[fail] Дам ему имя


[fail] Вот он на вьюшку репозитории


[fail] Теперь я могу завязать на него проект


[fail] Опять это дурацкое окошко - игнорим

[fail] Указываем новосозданнй репозиторий


[fail] Можно увидеть, каквозле проекта показалась пометка с именем репозитория


[fail] Теперь можно закоммититься


[fail] Указываем от имени кого коммит


[fail] Теперь выбираем что коммитим и с каким сообщением


[fail] Но, блин! Он взял мою папку проекта и переместил из apache рядом с локальным git репозиторием, в этом то и [fail]


[fail] Удалю репозиторий


[fail] Оставляя контент на месте


[fail] Перемещаю проект обратно в apache


[fail] Импортирую свой проект




[fail] Снова пытаюсь расшарить


[OK] Но на этот раз при виде окошка я поставлю снятую галочку


[OK] Проигнорирую все, что мне скажет умник плагин и создам репозиторий вместе проекта. Пока пусть будет так - все на кучу конечно (и апачи и настройки проекта и git), но потом разберемся.


[OK] Теперь можно жмакать finish


[OK] Папка осталась на месте, рядом создался git локальный репозиторий


[OK] Могу опять закоммититься :)


[OK] Тут вожно, что я не коммитил настройки проекта


[OK] И запаблишить изменения на сервак


[OK] На котором надо предвариетльно создать новый репозиторий




[OK] И указать его в настройках push операции


[OK] Далее я просто нажал все, что нажимается :)




[OK] И процесс пошел


[OK] Кажется успешно!


[OK] Таки да!


 Все, теперь у нас проект на git hub и мы можем продолжить tdd-ить. Об этом дальше...