суббота, 22 февраля 2014 г.

Тестируем калькулятор №3. Расширяем тестовое покрытие

Предыдущая серия была тут - http://programmingmindstream.blogspot.ru/2014/02/2.html

Все исходники доступны тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/DraftsAndScketches/SomeTestProjects/DummyCalculator/Chapter3/

В предыдущей серии мы добавили тест СЛОЖЕНИЯ.

Теперь добавим ещё три теста - ВЫЧИТАНИЯ, УМНОЖЕНИЯ и ДЕЛЕНИЯ.

Для начала немного переработаем наш TPlusTest.

Выделим ещё один АБСТРАКТНЫЙ тест - TOperationTest.

Вот он:

unit OperationTest;

interface

uses
  CalculatorGUITest,
  MainForm
  ;

type
  TOperation = (opAdd, opMinus, opMul, opDiv);

  TOperationTest = class(TCalculatorGUITest)
   protected
    procedure VisitForm(aForm: TfmMain); override;
    function  GetOp: TOperation; virtual; abstract;
  end;//TOperationTest

implementation

uses
  TestFrameWork,
  SysUtils
  ;

procedure TOperationTest.VisitForm(aForm: TfmMain);
const
 aA = 10;
 aB = 20;
begin
 aForm.Edit1.Text := IntToStr(aA);
 aForm.Edit2.Text := IntToStr(aB);
 case GetOp of
  opAdd:
  begin
   aForm.Button1.Click;
   Check(StrToFloat(aForm.Edit3.Text) = aA + aB);
  end;
  opMinus:
  begin
   aForm.Button2.Click;
   Check(StrToFloat(aForm.Edit3.Text) = aA - aB);
  end;
  opMul:
  begin
   aForm.Button3.Click;
   Check(StrToFloat(aForm.Edit3.Text) = aA * aB);
  end;
  opDiv:
  begin
   aForm.Button4.Click;
   Check(StrToFloat(aForm.Edit3.Text) = aA / aB);
  end;
 end;//case GetOp
end;

end.

А TPlusTest теперь принимает такой вид:

unit PlusTest;

interface

uses
  OperationTest
  ;

type
  TPlusTest = class(TOperationTest)
   protected
    function  GetOp: TOperation; override;
  end;//TPlusTest

implementation

uses
  TestFrameWork,
  SysUtils
  ;

function TPlusTest.GetOp: TOperation;
begin
 Result := opAdd;
end;

initialization
 TestFramework.RegisterTest(TPlusTest.Suite);

end.

Теперь добавляем TMinusTest, TMulTest и TDivTest:

unit MinusTest;

interface

uses
  OperationTest
  ;

type
  TMinusTest = class(TOperationTest)
   protected
    function  GetOp: TOperation; override;
  end;//TMinusTest

implementation

uses
  TestFrameWork,
  SysUtils
  ;

function TMinusTest.GetOp: TOperation;
begin
 Result := opMinus;
end;

initialization
 TestFramework.RegisterTest(TMinusTest.Suite);

end.

unit MulTest;

interface

uses
  OperationTest
  ;

type
  TMulTest = class(TOperationTest)
   protected
    function  GetOp: TOperation; override;
  end;//TMulTest

implementation

uses
  TestFrameWork,
  SysUtils
  ;

function TMulTest.GetOp: TOperation;
begin
 Result := opMul;
end;

initialization
 TestFramework.RegisterTest(TMulTest.Suite);

end.

unit DivTest;

interface

uses
  OperationTest
  ;

type
  TDivTest = class(TOperationTest)
   protected
    function  GetOp: TOperation; override;
  end;//TDivTest

implementation

uses
  TestFrameWork,
  SysUtils
  ;

function TDivTest.GetOp: TOperation;
begin
 Result := opDiv;
end;

initialization
 TestFramework.RegisterTest(TDivTest.Suite);

end.


И вот что получаем:


Что мы в итоге имеем?

Мы покрыли ВСЕ операции нашего калькулятора.

Так сказать "обеспечили минимальное тестовое покрытие" всех прецедентов использования нашего приложения.

Тут можно поговорить о том, что надо тестировать "разные наборы данных".

Или о тестировании "случайных наборов".

А также можно поговорить о тестировании граничных условий. Например деления на ноль.

Также можно говорить о тестировании валидации введённых данных.

Мы об этом поговорим в последующих постах.

Также мы поговорим об "изменении архитектуры". Мы уже почти вплотную подобрались к этому.

Поговорим. Но в следующих постах.

Пока возьму тайм-аут. А вы поглядите на то, что я вам предоставил.

Ну и почитайте вот что, если ещё не читали - http://programmingmindstream.blogspot.ru/2014/02/blog-post_4473.html

P.S. Попросили тут нарисовать UML-ДИАГРАММУ классов для данного приложения и его тестов. Это ИНТЕРЕСНО? НУЖНО? И можно ли обойтись ОДНОЙ лишь диаграммой классов? Или нужна ещё и sequence-диаграмма?

26 комментариев:

  1. Мне кажется, с наследованием ты переборщил. И кода больше, и сложнее. Ради такого примера точно не стоит.

    ИМХО

    ОтветитьУдалить
    Ответы
    1. Думаешь? Ну может быть. Я подумаю.

      Просто мне лично так - оказалось проще написать.

      Удалить
    2. Ну плюс - "мысли забежали несколько вперёд". Я думал о параметризации тестов. Может быть ЗРЯ. На ДАННОМ этапе.

      Удалить
    3. Да и привык я как-то "нарезать классы тонкими ломтями". Каждому по "маленькому кусочку ответственности".

      Удалить
    4. И ещё. В работе над тестами я лично предпочитаю из КОНКРЕТНЫХ тестов выделять АБСТРАКТНЫЕ (параметризуемые), чтобы из них потом сделать НОВЫЕ конкретные. И так по кругу. Так "мышечная масса" тестов очень хорошо нарастает. Опять же - может быть тут это и ПРЕЖДЕВРЕМЕННО. Но есть уже просто "наработанные стереотипы" мышления.

      Удалить
    5. Ну дело твое. Тут все абсолютно правильно, технически упрекать не в чем. Просто мне казалось, что цель данного конкретного примера - показать простой путь. Ну чтобы неофиты могли понять суть, не отвлекаясь на технически изощренные вещи.

      Удалить
    6. Ну для меня лично наследование (как впрочем и примеси) - это чисто "техническая вещь", а не концептуальная. Мне что один класс, что десять - всё едино. Я на классы смотрю лишь как на "зоны ответственности". Надеюсь, что наследование "неофитам" мозг не порвёт.

      Удалить
  2. Извините не понял, где мы TOperationTest поднимаем, и MainForm собственно ?

    ОтветитьУдалить
  3. Форма и тесты поднимаются в dpr на уровне запуска программы. Хотелось бы отделения формы от приложения. :)

    ОтветитьУдалить
    Ответы
    1. "Форма и тесты поднимаются в dpr на уровне запуска программы. Хотелось бы отделения формы от приложения. :)"

      -- я эту проблему решаю ОТДЕЛЬНЫМ приложением ТЕСТОВЫМ, которое КОПИРУЕТ ВЕСЬ функционал "настоящего" и генерируется из UML. "Обычным" же людям, у которых нет кодогенерации для начала могу порекомендовать использование IfDef.

      Я предвидел этот вопрос, но забыл...

      Удалить
  4. насколько я помню форму mdiChild нельзя сделать без главного окна, и стает вопрос как это сделать для тестирования ?

    ОтветитьУдалить
    Ответы
    1. "насколько я помню форму mdiChild нельзя сделать без главного окна, и стает вопрос как это сделать для тестирования ?"

      Это в копилку вот к чему - http://programmingmindstream.blogspot.ru/2014/02/2.html?showComment=1393364099651#c6835117592870444331

      Если форма создана ЗАКОННЫМИ СРЕДСТВАМИ приложения, то она - ТЕСТИРУЕМА.

      Если хочется что-то "эмулировать" - то можно "помудрить". Например не ставить MDIChild в Design-time, а ставить его в Run-time.

      А можно и не "мудрить", а сделать Factory Mathod или Dependency Injection.

      Удалить
  5. У меня для Вас плохие новости - в этом наследовании нет никакой абстракции.
    Не удивлюсь, если я не знаю чего-либо, что делает этот код "хорошим", но пока для меня это выглядит как "очень плохой" код ._.

    ОтветитьУдалить
    Ответы
    1. Son of a gun
      Как бы Вы написали это код "хорошо" ?

      Удалить
    2. >>У меня для Вас плохие новости - в этом наследовании нет никакой абстракции.

      Связывание "абстракции" неспосредственно с наследованием есть результат зауженного книжками мышления. Если учиться по книжкам, то всегда будешь чуть хуже автора книжек. В какой-то момент нужно чуть расширить сознание. Кстати, даже такой маститый гуру как Страуструп говорит, что "я придумал С++ так, как я его видел... но вы можете/должны применять его так, как вам удобно для решения ваших задач".

      Например: наследование есть ПРОСТО МЕХАНИЗМ РАЗДЕЛЕНИЯ КОДА. И не обязательно в контексте "абстрактное-конкретное". Все-таки "абстрактное-конкретное" отличается от "общее-частное".
      Молоток - общее, молоток с гвоздодёром - частное. А есть еще и с обрезиненной ручкой. Все три объекта (класса) весьма конкретны, но наследование здесь бесспорно.

      Давайте посмотрим на пример Александра. Идёт "операционное разделение" методов по категориям классов. Здесь слово "класс" (наконец) начинает означать то, что и должно. Элемент классификации.
      И классификация может быть многопараметрической, точнее может/должно быть МНОГО классификаций. Александр применил "наследование" в чистом виде как средство разделения (структуризации) кода.

      Опять же - интефейсы (как другой пример). Они - не абстрактны! Они - функционально конкретны. Что может быть функционально-конкретнее IUnknown? Но никто не протестует против "втискивание" интерфейсов в единый класс-реализацию. Согласитесь, здесь вообще нет "абстрактное-конкретное". Есть конкретный функциональный набор, разбросанный по разным I-интефейсам (чистым классам в С++), который конкретно в-реализуется в уже реальном классе (не абстрактном в плане без чисто виртуальных функций).

      И вот эта "свёртка" интерфейсов в реализационный класс и есть у Александра, только наоборот :) Т.е. правильно - сверху вниз, когда общий (но НЕ абстрактный) функционал распадается на конкретные функциональные конгломераты (объединенные общим предком). Это просто способ построить функционально-тестирующую модель в иерархическом виде.

      Вот и сказочке конец, кто не понял - F1.

      Удалить
  6. > Связывание "абстракции" неспосредственно с наследованием есть результат зауженного книжками мышления. Если учиться по книжкам, то всегда будешь чуть хуже автора книжек. В какой-то момент нужно чуть расширить сознание.

    Ко мне это не относится, я не учу по книгам. Когда я говорил об абстракции, я говорил об этом: "АБСТРАКТНЫЙ тест - TOperationTest.".

    >Давайте посмотрим на пример Александра. Идёт "операционное разделение" методов по категориям классов. Здесь слово "класс" (наконец) начинает означать то, что и должно. Элемент классификации.

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

    >Опять же - интефейсы (как другой пример). Они - не абстрактны!

    Интерфейс это механизм абстракции, не надо пустословить.

    >чистым классам в С++

    Не "чистым", а "чисто виртуальным" классам. Хотя обычно их называют "абстрактные классы", что как-бы намекает. =)

    >Как бы Вы написали это код "хорошо" ?

    Написал бы функцию, принимающую функцию от двух чисел и кнопку, в качестве своих параметров. Затем просто применил бы её для соответствующего набора кнопок и операторов.

    Пример на хаскеле: http://pastebin.com/RwkA2CTA

    ОтветитьУдалить
    Ответы
    1. "Написал бы функцию, принимающую функцию от двух чисел и кнопку, в качестве своих параметров. Затем просто применил бы её для соответствующего набора кнопок и операторов."

      -- вот тут - согласен.

      Удалить
    2. Одно только но. Которое я писал Роману. Я "думал вперёд" (что может быть и ПРЕЖДЕВРЕМЕННО) - об избавлении от нажатия на кнопки.

      Удалить
    3. Правда ОТДЕЛЬНОСТОЯЩАЯ функция - ничуть НЕ ЛУЧШЕ, функции класса. ОБА подхода - равноценны. Ничего "ужасного" ни в том, ни в другом - не вижу.

      Удалить
    4. Я кстати отдельным постом пожалуй напишу как преобразовать этот тест в несколько иной, с использованием:

      Buttons : array [TOperation] of TButton = (Button1, Button2, Button3, Button4);
      Ops : array [TOperation] of reference to function (a, b: double) : double = (function (a, b : bouble) : double begin result := a + b end, function (a, b : bouble) : double begin result := a - b end, function (a, b : bouble) : double begin result := a * b end, function (a, b : bouble) : double begin result := a / b end);

      Удалить
    5. >>Интерфейс это механизм абстракции

      Интерфейс - это механизм. Абстракция есть некая идея, которую можно поддержать "виртуальными" и "чисто виртуальными" методами, что есть "интерфейсы" в отдельной ООП концепции. Но называть "розетку" (=интерфейс) или "разъем USB" абстракцией... Однозначно сказывается "книжное" воспитание и нежелание мыслить чуть шире, чем забито в справочной системе по конкретным тулзам.

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

      >>Не "чистым", а "чисто виртуальным" классам.
      Слово "чистые" было взято в кавычки. Если уж следовать книжным традициям, то кавычки означают отход от жёсткой трактовки. Слабая попытка поймать на терминах, коллега.

      >>Хотя обычно их называют "абстрактные классы", что как-бы намекает. =)
      А Вы помните, почему их так называют? Только с точки зрения компилятора (и то не любого). Проблемы инстанцирования. Где-то warning с abstraction error, где-то compilation error. Никто ни на что не намекает, есть конретные проблемы компиляции и времени исполнения в случае инстанцирования "абстрактных" классов.

      Ещё раз - есть "абстракция" как способ избежания дублирования (поддержанная наследованием и полиморфизмом).
      Есть "абстракция" как классификационный признак при работе компилятора.
      Есть "абстракция" как метод умозрительной трактовки сущностей.

      Редкая книга пишет сразу об этом. Чаще всего, книжка описывает лишь один из трёх пунктов.

      >>Написал бы функцию
      Частица "бы" очень многое объясняет при трактовке мотивационной составляющей Ваших комментариев.

      Удалить
    6. >>и кнопку, в качестве своих параметров.

      А какую? FMX.StdCtrls.TButton или VCL.StdCtrls.TButton?
      Или абстрактную кнопку? :)

      Удалить
    7. >Интерфейс - это механизм

      Я тоже самое написал, читайте внимательнее. Дальнейшие рассуждения не имеют смысла.

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

      На хабре Вас не научили, что все эти Ваши игры в психологов, задорновщину и прочую угадайку, не работают? Ни на йоту же не попадаете. =)

      >Слабая попытка поймать на терминах, коллега.

      На самом деле не было никакой попытки, просто поправил. Спокойно.

      >А Вы помните, почему их так называют? Только с точки зрения компилятора (и то не любого). Проблемы инстанцирования

      Вы путаете причину и следствие. Абстрактные классы так называют прежде всего потому, что они призваны обеспечить слой абстракции.

      >Или абстрактную кнопку? :)

      Можно просто IO (), если хотите. =)
      Ввод (Fractional a, Num a) => a -> IO (), вывод (Fractional a, Num a) => IO a.
      Ещё глупые вопросы будут?

      Удалить
    8. >Ввод (Fractional a, Num a) => a -> IO (), вывод (Fractional a, Num a) => IO a.

      Можно просто a -> IO () и IO a. Так даже абстрактнее. =)

      Удалить
  7. Мне понравилось. Продолжайте, пожалуйста.

    ОтветитьУдалить
  8. :-) продолжу :-) обязательно продолжу

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