Как написать тест на делфи

Сейчас подготовил серию уроков по созданию тестов на Delphi. Эта тема сейчас очень распространненая, так как много кто пишет курсовые по этим темам, дипломные и многое другое. Тех же самых сотрудников можно протестировать, так что эта тема очень распространненая. Предлагаю серию тестов с использованием файлов. Да с использованием БД — это хорошо, но если БД локальная, то дела все теже, что и с файлами. С БД я даже рассказывать не буду как делать, там вообще все просто, тем более я рассказал курс по работе с БД в Delphi. Так вот рассмотрим создание тестов используя два варианта. С использованием обычных файлов — dat, db или любой другой свой формат, а также с использованием ini-файлов. Скажу одно, что с использованием ini-файлов необходимо шифрование данных, я про него еще не рассказывал, но все как-нибудь доберусь. Шифрование нужно хотя бы для, того, чтобы пользователь, открыв файл не узнал правильный ответ, в dat-файле например, это можно кое-как скрыть и без шифрование, так что давайте попробуем для начала добавить данные в файл. То есть добавить (создать) наш тест. Для начала нам необходимо описать структуру, где будет храниться наш тест, а затем создать файл данной структуры.
Моя структура выглядит так

Type
TTest=record
Name_v:String[255];
otv:string[255];
count_v:integer;
count_otv:integer;
pr_ot:string[255];
end;

Здесь у меня

  • Name_v — вопрос
  • otv — ответ
  • count_v — количество вопросов
  • count_otv — количество ответов
  • pr_ot — правильные ответы

Далее мы описываем файл данной стуктуры, затем саму стуктуру и начинаем запись. Я про файлы не рассказывал (а надо было, займусь этим попозже), тут есть набор функций и процедур по работе с файлами. На событие формы — OnCreate мы создаем наш файл тестов

procedure TForm1.FormCreate(Sender: TObject);
var
F:File of TTest;
begin
try
count:=0;
AssignFile(F,'test.dat');
Rewrite(F);
except
on e:Exception do
end;
end;

Функции и процедуры по работе с файлами я расскажу потом, а сейчас менее подробно. Например AssignFile — связывает файловую переменную с именем файла. Rewrite — создает новый файл, Reset — открывае уже существующий файл, Seek — устанавливает указатель в файле на нужную компоненты (она нам очень пригодится). Также на форму я установил следующие компоненты

  • TButton — 2 шт
  • TEdit — 2 шт
  • TCheckListBox
  • TGroupBox

То есть у нас будут сколько угодно вариантов ответа и сколько угодно правильных ответов. Далее на кнопку добавления ответов в TChecklistBox я написал следующий код.

procedure TForm1.Button4Click(Sender: TObject);
begin
try
CheckListBox1.Items.Add(Edit2.Text);
Edit2.Clear;
except
on e:Exception do
end;
end;

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

procedure TForm1.Button1Click(Sender: TObject);
var
i:integer;
F:File of TTest;
Test:TTest;
count,k:integer;
arr_pr_ot:array[1..255] of string;
begin
try
if (Trim(Edit1.Text)='') or (CheckListBox1.Count=0) then
begin
exit;
end;
inc(count);
Test.count_v:=3;
Test.count_otv:=CheckListBox1.Items.Count;
for i:=0 to CheckListBox1.Count-1 do
begin
Test.Name_v:=Edit1.Text;
if CheckListBox1.Checked[i] then
Test.pr_ot:=IntToStr(CheckListBox1.ItemIndex);
Test.otv:=CheckListBox1.Items[i];
Write(F,Test);
end;
if count=Test.count_v then
begin
ShowMessage('Test create compleate');
CloseFile(F);
count:=0;
end;
CheckListBox1.Clear;
Edit1.Clear;
except
on e:Exception do
end;
end;

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

Запись от krapotkin размещена 19.05.2018 в 18:28

Обновил(-а) krapotkin 19.05.2018 в 19:01

Давайте еще раз. Напишем очередной тест

Раз пошла такая пьянка, делаем полный разбор, начиная с проектирования
Что нам нужно знать о программе

Хранение данных.
Первое что надо обдумать. Сейчас думать не будем вообще, потому что уровень Beginner.
Пусть у нас есть текстовый файл, который содержит две строки в начале
первая — какой-то текст, вторая — число — количество вопросов в файле
а после этого идут блоки в соответстви с кол-вом вопросов. каждый блок состоит из 6 строк
текст вопроса
вариант ответа1
вариант ответа2
вариант ответа3
вариант ответа4
номер правильного ответа

после этого сразу блок следующего вопроса
В программе вопрос пусть хранится в записи

Delphi
1
2
3
4
5
TQuestion=record
  Q:string; // текст вопроса
  A:array[0..3] of string; // варианты ответа 1..4
  Right:integer; // номер правильного варианта 1..4
end;

соответственно, у нас есть массив вопросов

Delphi
1
var    Questions:array of TQuestion;

Думаю, что пусть будет и массив ответов

Delphi
1
var    Answers:array of integer;

чтобы все было совсем хорошо, создадим отдельный юнит File—New—Unit, куда сложим все наши классы
Комплект классов и переменных будет важно называться нашей моделью данных ))
кроме TQuestion пусть там еще лежит класс глобальных переменных программы TMySettings
и все процедуры, которые нужны для работы с моделью
назовем юнит UMyClasses.pas
обратим внимание на то, что этот юнит ничего не знает о форме, компонентах и прочей беде
только работа с самими данными

первая функция, которая туда попала — function ReadQuestionsFromFile(Filename:string):String;
на входе имя файла, на выходе — та самая первая строка из файла. ХЗ почему, просто так, чужой файл взял для образца
итого у нас сейчас вот такой файл UMyClasses.pas

Кликните здесь для просмотра всего текста

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
unit UMyClasses; 
 
interface
 
uses
  Classes, Types, Sysutils;
 
type
  TQuestion = record
    Q: string;
    A: array[0..3] of string;
    Right: integer;
  end;
 
  TDBSettings = record
    server: string;
    db: string;
  end;
 
  TMySettings = class
  public
    Login: string;
    Role: string;
    DBSettings: TDBSettings;
  end;
 
var
  Settings: TMySettings;
  Questions: array of TQuestion;
  Answers: array of integer;
 
function ReadQuestionsFromFile(FName: string): string;
 
implementation
 
function ReadQuestionsFromFile(FName: string): string;
var
  List: TStringList;
  VCount, I: integer;
begin
  List := TStringList.Create();
  List.LoadFromFile(FName); //прочитаем весь файл в список строк
  result := List[0]; // В первой строке у вас какой-то текст для заголовка
  VCount := StrToInt(List[1]); // во второй строке - кол-во записей
  SetLength(Questions, VCount);  // теперь,зная это количество, установим размер массива Questions
  SetLength(Answers, VCount); // и массива Answers
  I := 0;
  /// начиная с третьей строки, читаем вопросы
  while I < VCount do
  begin
    Questions[I].Q := List[2 + I * 6 + 0];
    Questions[I].A[0] := List[2 + I * 6 + 1];
    Questions[I].A[1] := List[2 + I * 6 + 2];
    Questions[I].A[2] := List[2 + I * 6 + 3];
    Questions[I].A[3] := List[2 + I * 6 + 4];
    Questions[I].Right := strToInt(List[2 + I * 6 + 5]);
    I := I + 1;
  end;
  List.Free;
end;
 
initialization
  Settings := TMySettings.Create;
 
finalization
  FreeAndNil(Settings);
 
end.

теперь немного об интерфейсе
сама тестовая форма выглядит так (Screen 166)
чтобы туда попасть,нужно пройти авторизацию
поэтому первая форма нашего приложения выглядит так (Screen 167)
и содержит интересный код

Кликните здесь для просмотра всего текста

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type
  TfMainForm = class(TForm)
    grp1: TGroupBox;
    eLogin: TEdit;
    ePass: TEdit;
    bOk: TButton;
    lbl1: TLabel;
    lbl2: TLabel;
    procedure bOkClick(Sender: TObject);
  private
    procedure GoTest;
  public
  end;
 
var
  fMainForm: TfMainForm;
 
implementation
 
uses
  UTestForm;
 
{$R *.dfm}
 
procedure TfMainForm.bOkClick(Sender: TObject);
begin
  // здесь будет проверка логина, но пока просто переход к тесту
  GoTest();
end;
 
procedure TfMainForm.GoTest;
var f:TfTestForm;
begin
  Hide;
  f:=TfTestForm.Create(self);
  try
    f.ShowModal;
  finally
    show;
    FreeAndNil(F);
  end;
end;

обратите внимание на то, что мы прячем главную форму
создаем и показываем тестовую
а потом опять показываем главную
это тоже не огонь, но для Beginner — в самый раз
кстати — TestForm удалена из Project options — Forms — Auto create !! ибо нефиг https://www.cyberforum.ru/blog… g4873.html

Форма для теста
Вернемся к тестовой форме
Заведем на ней поле — переменную — CurQuestion:integer;
Она будет изменяться от 0 до количества вопросов -1
И в пару к ней сделаем процедуру «отобразить текущий тест» ShowCurQuestion
Там прямо примитивно всё
Берем элемент массива Questions c индексом CurQuestion
и переписываем то что там хранится в компоненты на экране

Кликните здесь для просмотра всего текста

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
unit UTestForm;
 
interface
 
uses
  classes, types, forms, Controls, UMyClasses, ExtCtrls, StdCtrls;
 
type
 
  TfTestForm = class(TForm)
    pnl1: TPanel;
    lblQuestion: TLabel;
    bNext: TButton;
    rgVariants: TRadioGroup;
    pnlQuectionNo: TPanel;
    procedure FormCreate(Sender: TObject);
  private
    procedure ShowCurQuestion;
  public
    CurQuestion : integer;
  end;
 
var
  fTestForm: TfTestForm;
 
implementation
 
uses
  sysutils;
 
{$R *.dfm}
 
{ TMainForm }
procedure TfTestForm.FormCreate(Sender: TObject);
var
  Filename: string;
begin
  Filename := extractFilePath(ParamStr(0))+'questions.dat';
  Caption := ReadQuestionsFromFile(Filename);
  CurQuestion := 0;
  ShowCurQuestion;
end;
 
procedure TfTestForm.ShowCurQuestion;
var
  i: Integer;
begin
  pnlQuectionNo.Caption := Format('Вопрос %d / %d',[curQuestion+1, Length(Questions)]);
  lblQuestion.Caption := questions[CurQuestion].Q;
  rgVariants.Items.Clear;
  for i := 0 to 3 do
    rgVariants.Items.Add(questions[CurQuestion].A[i]);
  rgVariants.ItemIndex := -1;
end;
end.

теперь если на кнопку Далее повесить обработчик, который сохраняет ответ пользователя в массив Answers, просто увеличивает CurQuestion и просит форму нарисовать вопрос, то в общем-то тестовая часть нашего проекта завершена!

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
procedure TfTestForm.bNextClick(Sender: TObject);
begin
  // проверим, вдруг ничего не выбрано
  if rgVariants.ItemIndex=-1 then
  begin
    showMessage('Выберите вариант ответа');
    exit;
  end;
 
  Answers[CurQuestion] := rgVariants.ItemIndex;
  if CurQuestion<Length(Questions)-1 then
  begin
    CurQuestion := CurQuestion + 1;
    ShowCurQuestion;
  end;
end;

хорошая идея если мы нажали Далее на последнем вопросе, то закрыть форму с тестом и вывести форму Итого, и возможно даже записать результаты в к-нить файл или даже БД, но это следующий урок.

Приложу исполняемый файл и файл с вопросами, чтобы можно было посмотреть, что получилось…

Создание теста.

Компонент RadioGroup

Caption заголовок группы

Items задание строк
кнопок 

ItemIndex содержит
номер выбранной строки (нумерация с 0),
значение -1 означает что ни одна строка
не выбрана!

1.

2.

3.

4.

5.


RadioGroup

Варианты ответов
(список) создаем в свойстве Items
(RadioGroup при этом выделен)

Тест состоит, например, из 5 форм. На
каждой форме расположены вопрос, варианты
ответов, кнопка (New – Form
открытие новой формы, Shift
+ F12 перемещение между
формами).

Процедура пишется на кнопку Далее.
Во всей работе будет одна переменная
i: integer;
она глобальная. Описываем ее в месте,
где:

Var

Form1: TForm1;

i:integer;
вписываем сами

Implementation

Uses Unit2;

Описание глобальной переменной происходит
только в Unit1. Разработчик
теста должен знать правильный ответ,
т.к. это указывается в программе.

Процедура на
кнопку Далее:

Begin

If RadioGroup1.Itemindex=-1 then ShowMessage
(‘Забыли ответить
на вопрос!’)

Else

Begin if RadioGroup1.Itemindex=2 then i:=i+1;

Form2.show;

End;

Открываем новую форму (New
– Form) оформляем вопрос –
ответы, кнопка Далее:

Begin

If RadioGroup1.Itemindex=-1 then ShowMessage
(‘Забыли ответить
на вопрос!’)

Else

Beginif RadioGroup1.Itemindex=1 then i:=i+1;

Form2.Close;

Form3.Show;

End;

Начиная со
второго модуля
(Unit2), добавляем слово
Unit1 в то
место, где
идет перечисление:
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,

StdCtrls, ExtCtrls,unit1;

Аналогичную работу проделываем с формой
3 и 4.

На форме 5 располагаем вопрос, варианты
ответов, кнопку Готово и пишем процедуру
на эту кнопку

Begin

If RadioGroup1.Itemindex=-1 then ShowMessage
(‘Забыли ответить
на вопрос!’)

Else

Begin if RadioGroup1.Itemindex=3 then i:=i+1;

End;

Case i of

0,1,2: ShowMessage (‘Вы получили
2’);

3: ShowMessage (‘Вы получили
3’);

4: ShowMessage (‘Вы получили
4’);

5: ShowMessage (‘Вы получили
5’);

End;

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

Первую форму
можно использовать для отображения
общей информации о тесте, например:

Используемые объекты:

Компонента

Свойство
(properties)

Значение

Events

Forma1

Caption

Методика
многофакторного исследования личности
Р. Кеттела

OnCreate

Memo1

Lines

очитстить

Font

Настроить
по своему вкусу

ScroollBars

ssVertical

Button1

Caption

Ok

OnClick

Весь текст отображаемый в объекте Memo1,
заранее сохранить в файле text.txt (название
может быть свое), в текущей папке проекта

procedure TForm1.FormCreate(Sender: TObject);

begin

Memo1.Lines.LoadFromFile (‘text.txt’);

end;

procedure TForm1.Button1Click(Sender: TObject);

begin

Form2.show;

end;

Добавление иллюстраций

Страница
библиотеки Additional
Класс TImage

Компонент
Image
дает
отображение на форме графического
изображения. Свойство Picture
позволяет
отобразить выбранное изображение в
приложении.

Св-во

Объявление
/ Описание

AutoSize

Указывает,
изменяется ли автомататом размер
компонента, подстраиваясь под размер
изображения.
По умолчанию значение
false — не подстраивается.

Center

Указывает,
должно ли изображение центрироваться
в поле компонента,
если
его размеры меньше размеров поля. При
значении false изображение располагается
в
верхнем
левом углу поля. Свойство не действует,
если AutoSize установлено в true или если
Stretch
установлено
в true и Picture содержит не пиктограмму.

Соседние файлы в папке №10

  • #
  • #

Предлагаемые методические рекомендации
отображают проектирование, оформление и
разработку тестирующих программ в среде
программирования Delphi.

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

Введение

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

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

В результате использования этих рекомендаций
могут быть сформированы умения и навыки
проектирования тестирующих программ в среде
программирования Delphi.

Создание тестирующей программы с
использованием компонентов

TLabel, TButton, TRadioGroup

Требования к предварительной подготовке:

  • знать основы программирования в системе
    Паскаль;
  • уметь пользоваться кнопками панелей
    инструментов и контекстным меню.

Создание интерфейса приложения

1. Создайте проект 1 (презентация).

2. Задайте объектам следующие свойства

Объект Свойство Значение
Форма Name Form1
  Caption Тест самопроверки
  Color cllnfoBk
RadioGroup Name RadioGroup1, :, RadioGroup5
  Caption
  Items :(варианты ответов)
  ItemIndex 0,1,2,3 (индекс выбранного переключателя)
  Font Times New Roman, обычный, размер 14, цвет
тёмно-синий
Label    
(метка) Name Label1,:, Label5
  Caption :(текст выбранного вопроса)
  Font Times New Roman, полужирный, размер 14, цвет
тёмно-синий
Label    
(метка) Name Label4
  Caption
  Font Times New Roman, полужирный, размер 14, цвет
красный
Label    
(метка) Name Label5
  Caption
  Font Times New Roman, полужирный, размер 14, цвет
красный
Button    
(кнопка) Name Button1
  Caption Число правильных ответов
Button    
(кнопка) Name Button2
  Caption Выход

3.Сохраните проект (File -> Save All) в своей папке.

Написание кода

1. Напишите программу теста в окне кода.

var

Form1: TForm1;

m,n,k,l,w,v:integer; {указание типа переменных
(правильных ответов)}

implementation

{$R *.dfm}

{проверка правильности ответов на поставленные
вопросы}

procedure TForm1.RadioGroup1Click(Sender: TObject);

begin m:=0; if RadioGroup1.ItemIndex=1 then m:=m+1 else m:=m; end;

procedure TForm1.RadioGroup2Click(Sender: TObject);

begin n:=0; if RadioGroup2.ItemIndex=0 then n:=n+1 else n:=n; end;

procedure TForm1.RadioGroup3Click(Sender: TObject);

begin k:=0; if RadioGroup3.ItemIndex=0 then k:=k+1 else k:=k; end;

procedure TForm1.RadioGroup4Click(Sender: TObject);

begin w:=0; if RadioGroup4.ItemIndex=0 then w:=w+1 else w:=w; end;

procedure TForm1.RadioGroup5Click(Sender: TObject);

begin v:=0; if RadioGroup5.ItemIndex=2 then v:=v+1 else v:=v; end;

{подсчёт правильных ответов и вывод результата
тестирования}

procedure TForm1.Button1Click(Sender: TObject);

begin l:=m+n+k+w+v; if l=5 then

begin Label4.Caption:=IntToStr(l); Label5.Caption:=’Отлично!’; end else if l=4
then

begin Label4.Caption:=IntToStr(l); Label5.Caption:=’Хорошо! Но Вам
необходимо повторить теоретический материал.’; end
else if l<=3 then

begin Label4.Caption:=IntToStr(l); Label5.Caption:=’Плохо! Учите!’ end; end;

{выход из программы}

procedure TForm1.Button2Click(Sender: TObject); begin close; end; end.

2.Сохраните форму и проект в своей папке.

Создание тестирующей программы с
использованием компонентов TLabel, TButton, TRadioGroup,
TMainMenu

Создание интерфейса приложения

1.Создайте проект 2 (презентация).

Для этого добавьте на форму следующие
компоненты:

  • компонент MainMenu, состоящий из пунктов: Тест
    (Выход) и Помощь (Инструкция, О программе);
  • метку для текста вопросов и вывода результата
    теста в виде строки «:правильных ответов из 10
    вопросов»;
  • метку «Следующий ответ»;
  • группу зависимых переключателей для вариантов
    ответов (RadioGroup1,:, RadioGroup10), которые накладываются
    друг на друга;
  • кнопку «Результат тестирования».

2. Задайте объектам следующие свойства.

Объект Свойство Значение
Форма Name Form1
  Caption Тест по информатике
  Color clSkyBlue
MainMenu Name MainMenu1
Label    
(метка) Name Label1
  Caption :(текст вопроса)
  Font MS Sans Serif, полужирный курсив, размер 14,
цвет тёмно-синий
Label    
(метка) Name Label2
  Caption Следующий вопрос
  Font MS Sans Serif, полужирный, размер 14, цвет
сиреневый
Button    
(кнопка) Name Button1
  Caption Выход
  Font MS Sans Serif, полужирный курсив, размер 14,
цвет чёрный
Button    
(кнопка) Name Button2
  Caption Результат тестирования
  Font MS Sans Serif, полужирный курсив, размер 14,
цвет чёрный
  Visible False
RadioGroup Name RadioGroup1, :, RadioGroup10
  Caption
  Items :(варианты ответов)
  ItemIndex 0,1,2,3 (индекс выбранного переключателя)
  Font Times New Roman, обычный, размер 14, тёмно-синий
  Visible для RadioGroup1 — True

для RadioGroup2,:, RadioGroup10 —
False

3.Сохраните проект (File -> Save All) в своей папке.

Написание кода

1. Запрограммируйте метку Label2 («Следующий
вопрос») так, чтобы в метке Label1 выводился
следующий вопрос, варианты ответа на него, и при
достижении последнего вопроса метка Label2
становилась недоступной.

2. При написании программы следуйте инструкции:

  • повторный выбор ответа на вопрос недоступен;
  • результат тестирования вывести в метке Label1 по
    щелчку кнопки Button2;
  • пункт главного меню «Выход» закрывает
    программу;
  • пункт главного меню «О программе» выводит окно
    формы About Box (для этого необходимо выполнить
    команду File -> New-> Other, закладка Forms, форма About );
  • пункт главного меню «Инструкция» выводит
    информационное окно.

3.Напишите программу теста в окне кода.

Var Form1: TForm1;

{указание типа переменных (правильных ответов)}

a,b,c,d,f,g,j,l,m,n,k,p:integer; implementation uses Unit2; {$R *.dfm}

{проверка правильности ответов на поставленные
вопросы}

procedure TForm1.RadioGroup1Click(Sender: TObject);

begin a:=0; if RadioGroup1.ItemIndex=0 then a:=a+1 else a:=a;
RadioGroup1.Visible:=False; end;

procedure TForm1.RadioGroup2Click(Sender: TObject);

begin b:=0; if RadioGroup2.ItemIndex=1 then b:=b+1 else b:=b;
RadioGroup2.Visible:=False; end;

procedure TForm1.RadioGroup3Click(Sender: TObject);

begin d:=0; if RadioGroup3.ItemIndex=1 then d:=d+1 else d:=d;
RadioGroup3.Visible:=False; end;

procedure TForm1.RadioGroup4Click(Sender: TObject);

begin c:=0; if RadioGroup4.ItemIndex=0 then c:=c+1 else c:=c;
RadioGroup4.Visible:=False; end;

procedure TForm1.RadioGroup5Click(Sender: TObject);

begin f:=0; if RadioGroup5.ItemIndex=3 then f:=f+1 else f:=f;
RadioGroup5.Visible:=False; end;

procedure TForm1.RadioGroup6Click(Sender: TObject);

begin g:=0; if RadioGroup6.ItemIndex=2 then g:=g+1 else g:=g;
RadioGroup6.Visible:=False; end;

procedure TForm1.RadioGroup7Click(Sender: TObject);

begin j:=0; if RadioGroup7.ItemIndex=1 then j:=j+1 else j:=j;
RadioGroup7.Visible:=False; end;

procedure TForm1.RadioGroup8Click(Sender: TObject);

begin l:=0; if RadioGroup8.ItemIndex=1 then l:=l+1 else l:=l;
RadioGroup8.Visible:=False; end;

procedure TForm1.RadioGroup9Click(Sender: TObject);

begin m:=0; if RadioGroup9.ItemIndex=3 then m:=m+1 else m:=m;
RadioGroup9.Visible:=False; end;

procedure TForm1.RadioGroup10Click(Sender: TObject);

begin n:=0; if RadioGroup10.ItemIndex=2 then n:=n+1 else n:=n;
RadioGroup10.Visible:=False; end;

{вывод вопроса и вариантов ответа в метке Label1 по
щелчку метки Label2(«Следующий вопрос»)}

procedure TForm1.Label2Click(Sender: TObject);

begin k:=k+1; case k of

1: begin Label1.Caption:=’2 вопрос. Массовое производство
компьютеров началось в:’;

RadioGroup2.Visible:=True; end;

2: begin Label1.Caption:=’3 вопрос. Элементной базой
процессоров ЭВМ 2-ого поколения являлись:’;
RadioGroup3.Visible:=True; end;

3:begin Label1.Caption:=’4 вопрос. Какое устройство не
предназначено для обработки информации?’;
RadioGroup4.Visible:=True; end;

4:begin Label1.Caption:=’5 вопрос. За минимальную единицу
измерения количества информации принят:’;
RadioGroup5.Visible:=True; end;

5:begin Label1.Caption:=’6 вопрос. Сколько байтов занимает
в памяти ПК слово ПОБЕДА?:’;

RadioGroup6.Visible:=True; end;

6:begin Label1.Caption:=’7 вопрос. Производительность
работы компьютера зависит от:’;

RadioGroup7.Visible:=True; end;

7:begin Label1.Caption:=’8 вопрос. Какое устройство может
оказывать вредное воздействие на здоровье
человека?’; RadioGroup8.Visible:=True; end;

8:begin Label1.Caption:=’9 вопрос. При выключении
компьютера вся информация стирается:’;

RadioGroup9.Visible:=True; end;

9:begin Label1.Caption:=’10 вопрос. В целях сохранения
информации дискеты необходимо оберегать от:’;
RadioGroup10.Visible:=True; Button2.Visible:=True;Label2.Visible:=False; end; end;

end;

{подсчёт правильных ответов и вывод результата
тестирования}

procedure TForm1.Button2Click(Sender: TObject);

begin p:= a+b+c+d+f+g+j+l+m+n; if p=10 then

begin Label1.Caption:=IntToStr(p)+’ правильных ответов из 10
вопросов.’+’ Отлично!’; end

else if p>=8 then

begin Label1.Caption:=IntToStr(p)+’ правильных ответов из 10
вопросов.’+’ Хорошо!’; end

else if p=7 then

begin Label1.Caption:=IntToStr(p)+’ правильных ответов из 10
вопросов.’+’ Удовлетворительно.’;

end else if p<7 then

begin Label1.Caption:=IntToStr(p)+’ правильных ответов из 10
вопросов.’+’ Плохо! Учите!’; end;

end;

{выход из программы по кнопке Button1}

procedure TForm1.Button1Click(Sender: TObject); begin close; end;

{выход из программы по щелчку пункта главного
меню «Выход»}

procedure TForm1.N2Click(Sender: TObject);

begin close; end;

{подключение формы AboutBox}

procedure TForm1.N4Click(Sender: TObject);

begin AboutBox.show; end;

{вывод информационного окна по пункту главного
меню «Инструкция»}

procedure TForm1.N5Click(Sender: TObject);

begin MessageDlgPos(‘Будьте внимательны при выборе
варианта ответа! Повторный выбор ответа на
вопрос недоступен!’,mtInformation,[mbOK],0,300,200); end; end.

4.Сохраните форму и проект в своей папке.

Создание тестирующей программы с
использованием компонентов

TLabel, TButton, TRadioGroup, TPageControl, TImage, TPanel, TMainMenu

Создание интерфейса приложения

1.Создайте проект 3 (презентация).

Для этого добавьте на форму следующие
компоненты:

  • компонент MainMenu, состоящий из пунктов: Тест
    (Результат, Выход) и Помощь (Инструкция, О
    программе);
  • метку для вывода результата теста в виде строки
    «:правильных ответов из 6 вопросов»;
  • компонент PageControl (страница WIN32), который содержит
    вкладки с номерами вопросов;
  • на каждую вкладку поместите метку с текстом
    вопроса и группу зависимых переключателей для
    вариантов ответов (RadioGroup) с 3-4 вариантами ответов;
  • кнопку выхода из программы;
  • компонент Image (страница ADDITIONAL);
  • шесть компонентов Panel разместить таким образом,
    чтобы закрыть компонент Image.

2.Задайте объектам следующие свойства.

Объект Свойство Значение
Форма Name Form1
  Caption Тестирующая программа по информатике
  Color clSkyBlue
MainMenu Name MainMenu1
Label    
(метка) Name Label1
  Caption
  Font MS Sans Serif, полужирный курсив, размер 14,
цвет красный
Label    
(метка) Name Label2,:, Label7
  Caption :(текст вопроса)
  Font MS Sans Serif, полужирный курсив, размер 12,
цвет тёмно-синий
Button    
(кнопка) Name Button1
  Caption Выход
  Font MS Sans Serif, полужирный курсив, размер 14,
цвет чёрный
Image Name Image1
  Visible True
  Stretch True
  Picture указать путь к рисунку
Panel Name Panel1,:,Panel6
  Caption 1,:6
PageControl Name PageControl1
  Active Pages TabSheet1
  Visible True
TabSheet1,:, TabSheet6 Name TabSheet1,:,TabSheet6
  Caption вопрос1,:,вопрос6
RadioGroup Name RadioGroup1, :, RadioGroup6
  Caption
  Items :(варианты ответов)
  ItemIndex 0,1,2,3 (индекс выбранного переключателя)

3.Сохраните проект (File -> Save All) в своей папке.

Написание кода

1. При написании кода программы следуйте
инструкции:

  • повторный выбор ответа на вопрос недоступен;
  • при правильном выборе ответа на вопрос
    открывается часть рисунка (компонент Image);
  • пункт главного меню «Результат» выводит в
    метке Label1 результат тестирования;
  • пункт главного меню «Выход» закрывает
    программу;
  • пункт главного меню «О программе» выводит окно
    формы About Box (для этого необходимо выполнить
    команду File -> New-> Other, закладка Forms, форма About );
  • пункт главного меню «Инструкция» выводит
    информационное окно.

2. Напишите программу теста в окне кода.

var Form1: TForm1;

{указание типа переменных (правильных ответов)}

m,n,k,w,l,s,z:integer; implementation uses Unit2; {$R *.dfm}

{выход из программы по кнопке Button1}

procedure TForm1.Button1Click(Sender: TObject);

begin close; end;

{проверка правильности ответов на поставленные
вопросы и при правильном выборе ответа на вопрос
открытие части рисунка }

procedure TForm1.RadioGroup1Click(Sender: TObject);

begin n:=0; if RadioGroup1.ItemIndex=0 then begin n:=n+1; Panel1.Visible:=False; end
else n:=n;

RadioGroup1.Visible:=False; end;

procedure TForm1.RadioGroup2Click(Sender: TObject);

begin m:=0; if RadioGroup2.ItemIndex=2 then begin m:=m+1;Panel2.Visible:=False; end
else m:=m;

RadioGroup2.Visible:=False; end;

procedure TForm1.RadioGroup3Click(Sender: TObject);

begin k:=0; if RadioGroup3.ItemIndex=0 then begin k:=k+1; Panel4.Visible:=False; end
else k:=k;

RadioGroup3.Visible:=False; end;

procedure TForm1.RadioGroup4Click(Sender: TObject);

begin w:=0; if RadioGroup4.ItemIndex=0 then begin w:=w+1; Panel3.Visible:=False; end
else w:=w;

RadioGroup4.Visible:=False;end;

procedure TForm1.RadioGroup5Click(Sender: TObject);

begin s:=0; if RadioGroup5.ItemIndex=1 then begin s:=s+1; Panel5.Visible:=False; end
else s:=s;

RadioGroup5.Visible:=False; end;

procedure TForm1.RadioGroup6Click(Sender: TObject);

begin z:=0; if RadioGroup6.ItemIndex=1 then begin z:=z+1; Panel6.Visible:=False; end
else z:=z;

RadioGroup6.Visible:=False;end;

{подсчёт правильных ответов и вывод результата
тестирования по пункту главного меню
«Результат»}

procedure TForm1.N3Click(Sender: TObject);

begin l:=m+n+k+w+s+z; Label1.Caption:=IntToStr(l)+’ правильных ответов
из 6 вопросов’; end;

{выход из программы по пункту главного меню
«Выход»}

procedure TForm1.N4Click(Sender: TObject);

begin close; end;

{подключение формы AboutBox}

procedure TForm1.N7Click(Sender: TObject);

begin AboutBox.show; end;

{вывод информационного окна по пункту главного
меню «Инструкция»}

procedure TForm1.N6Click(Sender: TObject);

begin MessageDlgPos(‘Будьте внимательны при выборе
варианта ответа!

Повторный выбор ответа на вопрос
недоступен!’,mtInformation,[mbOK],0,300,200); end; end.

3.Сохраните форму и проект в своей папке.

Литература

1. Анеликова Л.А. Тесты. Информатика и
информационные технологии. — Москва: Дрофа, 2004.

2. Валеева Ю.А. Объектно-ориентированное
программирование в среде Delphi. — Новокузнецк, 2003.

3. Дробахина А.Н., Можаров М.С. Структурирование
содержания профессиональной подготовки учителя
информатики средствами современного
гипертекста. — Новокузнецк, 2003.

Разработка через тестирование в Delphi производится с помощью встроенного инструмента DUnit. В статье мы рассмотрим, как создаются тестовые проекты Delphi, как создавать юнит тесты и как тестировать.

Итак, сначала поговорим о том, что такое DUnit. DUnit – это инструмент тестирования с открытыми исходными кодами, основанный на JUnit. Доступен он как для Delphi, так и для C++.

Вообще, в состав Delphi этот инструмент включён начиная с Delphi 2005. Для Delphi и C++ Builder DUnit устанавливается автоматически установщиком RAD Studio. В папке sourceDUnit (внутри папки, куда установлен Delphi) вы можете найти много ресурсов, в том числе исходные файлы, документацию и примеры тестов. Поставляется DUnit под лицензией Mozilla Public License 1.1 (MPL).

В статье я не буду углубляться в теорию, а лишь покажу, как пользоваться инструментом DUnit в Delphi. Будем считать, что читатель знает, что такое разработка через тестирование.

Создание тестового проекта

Тестовый проект содержит один или несколько тестовых случаев, которые представляют из себя обычные .pas файлы и будут доступны в IDE на панели Project Manager. Также RAD Studio предоставляет в ваше распоряжение мастер создания тестового проекта «Test Project Wizard». Рекомендуется создавать два отдельных проекта: один тестируемый, а второй тестирующий. Так вам не придётся в будущем удалять ваши тесты из готового приложения.

Давайте для начала создадим проект, который мы будем тестировать. Допустим, это будет оконное VCL приложение. Выберите пункт меню «File -> New -> VCL Forms Application — Delphi». Созданный проект сохраните.

После создания тестируемого проекта, создадим тестовый проект. Для этого выберите пункт меню «File -> New -> Other…», затем в диалоге «New Items» выберите «Unit Test -> Test Project» и нажмите «OK».

Создание тестового проекта в Delphi

На первом шаге мастера «Test Project Wizard» в поле «Source Project» можно указать тестируемый проект, если их несколько. В полях «Project Name» и «Location» указывается название и расположение тестового проекта. В поле «Personality» выбирается язык программирования (в нашем случае – это Delphi). Все перечисленные поля заполнились автоматически, что нам подходит. Галочку «Add to project group» оставьте, чтобы проект добавился в текущую группу проектов. Нажмите «Next >».

Создание тестового проекта: Шаг 1

На следующем шаге можно выбрать, как будет выполняться тест (поле «Test Runner»): в окне («GUI») или в консоли («Console»). Оставим здесь предложенный по умолчанию вариант – «GUI». В поле «Test Framework» указываются инструменты тестирования. Поменять в этом поле ничего нельзя, т.к. для Delphi и C++ поддерживается только инструмент DUnit. Нажмите «Finish» и вы увидите, что в группе проектов появился новый пустой тестовый проект.

Создание тестового проекта: Шаг 2

Тестовые случаи и тестирование

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

В основном рекомендуется создавать тесты в отдельном проекте (отдельно от тестируемого проекта). Так вам не нужно будет удалять тесты из проекта перед финальной сборкой проекта.

RAD Studio предоставляет вам мастер «Test Case Wizard» для помощи в создании тестовых случаев, которые вы сможете настроить на своё усмотрение.

Прежде чем сделать тестовый случай, давайте определимся, что мы будем тестировать. В тестируемом проекте Project1, который мы создали, есть форма. Давайте её и будем тестировать. Допустим, наша форма будет построчно сравнивать два текстовых файла и показывать строки с различиями. Сравнивать будем двумя способами: учитывая регистр и без учёта регистра. Код для теста сделаем элементарный, будем сравнивать строки с одинаковыми индексами.

Итак, на форму ставим два текстовых поля TRichEdit и две кнопки TButton. Затем обрабатываем события от нажатий кнопок и дописываем код следующим образом:

unit Unit1;
 
interface
 
uses
   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
   System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
   Vcl.StdCtrls, Vcl.ComCtrls, System.Generics.Collections;
 
type
   TForm1 = class(TForm)
      RichEdit1: TRichEdit;
      RichEdit2: TRichEdit;
      Button1: TButton;
      Button2: TButton;
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
      procedure Button1Click(Sender: TObject);
      procedure Button2Click(Sender: TObject);
   private
   public
      //Список индексов разных строк.
      differentStrings: TList<integer>;
      //Процедура сравнения текста.
      procedure Compare(ignoreCase: boolean);
      //Процедура подкрашивания различающихся строк.
      procedure ShowDifferences;
   end;
 
var
   Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
uses Math;
 
procedure TForm1.Button1Click(Sender: TObject);
begin
   //Вызываем процедуру сравнения текста.
   Compare(false);
   //Вызываем процедуру подкрашивания различающихся строк.
   ShowDifferences;
end;
 
procedure TForm1.Button2Click(Sender: TObject);
begin
   //Вызываем процедуру сравнения текста.
   Compare(true);
   //Вызываем процедуру подкрашивания различающихся строк.
   ShowDifferences;
end;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
   differentStrings := TList<integer>.Create;
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
   differentStrings.Free;
end;
 
procedure TForm1.Compare(ignoreCase: boolean);
begin
 
end;
 
procedure TForm1.ShowDifferences;
 
   function GetSelStart(const text: TStrings; const lineIndex: integer): integer;
   var
      i: integer;
   begin
      //Вычисляем индекс первого символа указанной строки текста.
      Result := 0;
      for i := 0 to lineIndex - 1 do
         Result := Result + Length(text[i]) + 1;
   end;
 
   procedure ShowTextLines(const richEdit: TRichEdit);
   var
      index: integer;
   begin
      //Подкрашиваем все строки в чёрный цвет.
      richEdit.SelectAll;
      richEdit.SelAttributes.Color := clBlack;
      //Подкрашиваем разницу.
      for index in differentStrings do
         if index < richEdit.Lines.Count then
         begin
            richEdit.SelStart := GetSelStart(richEdit.Lines, index);
            richEdit.SelLength := Length(richEdit.Lines[index]);
            richEdit.SelAttributes.Color := clRed;
         end;
   end;
 
begin
   //Подкрашиваем разницу в текстовом поле 1.
   ShowTextLines(RichEdit1);
   //Подкрашиваем разницу в текстовом поле 2.
   ShowTextLines(RichEdit2);
 
 
   RichEdit1.PlainText := true;
   ShowMessage(RichEdit1.Lines.Text);
   RichEdit1.PlainText := false;
   RichEdit1.Lines.SaveToFile('c:temptst.txt');
   ShowMessage(RichEdit1.Lines[RichEdit1.Lines.Count - 1]);
end;
 
end.

Как видите, по нажатию на кнопки будут вызываться две процедуры Compare и ShowDifferences. Первая процедура будет сравнивать два текста и сохранять индексы несовпадающих строк в список differentStrings, а вторая процедура будет на основе этого списка подкрашивать несовпадающие строки в красный цвет. В процедуру Compare будет передаваться параметр ignoreCase определяющий способ сравнения строк. Пока реализацию функции Compare делать не будем, а сразу сделаем тест для неё.

Для создания тестов нужно сделать следующие шаги:

        • Откройте юнит, для которого нужно сделать тесты и переключитесь на закладку «Code». Т.е. ваш юнит должен отображаться в редакторе кода.
        • Выберите пункт меню «File -> New -> Other…».
        • В диалоге «New Items» выберите «Unit Tests -> Test Case» и нажмите «OK».

Создание тестового случая в Delphi

        • В открывшемся мастере «Test Case Wizard» на первом шаге нужно указать путь к файлу тестируемого юнита в поле «Source file» (сюда автоматически подставляется путь к файлу для текущего открытого в редакторе юнита) и выбрать, какие классы и методы нам нужно тестировать. В нашем примере будем тестировать только один метод Compare, поэтому поставьте галочку только напротив него. Нажмите «Next >».

Создание тестового случая в Delphi: Выбор тестируемых методов

        • На следующем шаге задаётся тестовый проект в поле «Test Project», имя файла тестового случая в поле «File name», инструменты тестирования в поле «Test Framework» (это поле неактивно, т.к. поддерживается только DUnit) и базовый класс в поле «Base class». Базовый класс TTestCase подходит для большинства случаев, но вы можете использовать и собственный класс. В нашем примере мы не будем здесь ничего менять. Нажмите кнопку «Finish».

Создание тестового случая в Delphi: Шаг 2

После этого вы увидите, что в тестовом проекте появился файл «TestUnit1.pas» и в этот же проект добавлен тестируемый юнит «Unit1.pas».

Тестовый проект в IDE Delphi

А вот что вы увидите в файле TestUnit1.pas:

unit TestUnit1;
{
 
   Delphi DUnit Test Case
   ----------------------
   This unit contains a skeleton test case class generated by the Test Case Wizard.
   Modify the generated code to correctly setup and call the methods from the unit 
   being tested.
 
}
 
interface
 
uses
   TestFramework, System.SysUtils, Vcl.Graphics, System.Generics.Collections,
   Vcl.StdCtrls, Winapi.Windows, System.Variants, System.Classes, Vcl.ComCtrls,
   Vcl.Dialogs, Vcl.Controls, Vcl.Forms, Winapi.Messages, Unit1;
 
type
   // Test methods for class TForm1
 
   TestTForm1 = class(TTestCase)
   strict private
      FForm1: TForm1;
   public
      procedure SetUp; override;
      procedure TearDown; override;
   published
      procedure TestCompare;
   end;
 
implementation
 
procedure TestTForm1.SetUp;
begin
   FForm1 := TForm1.Create;
end;
 
procedure TestTForm1.TearDown;
begin
   FForm1.Free;
   FForm1 := nil;
end;
 
procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
begin
   // TODO: Setup method call parameters
   FForm1.Compare(ignoreCase);
   // TODO: Validate method results
end;
 
initialization
   // Register any test cases with the test runner
   RegisterTest(TestTForm1.Suite);
end.

Как видите, здесь сделана заготовка для тестирующего класса TestTForm1, унаследованного от класса TTestCase. В секции initialization происходит регистрация этого класса.

В методе SetUp происходит подготовка к тестированию. Здесь нужно создать тестируемый класс и подготовить всё что нужно для тестов, например, подключиться к удалённому серверу или базе данных и т.п. Кстати здесь код сгенерирован с ошибкой: конструктор формы должен принимать входной параметр. Исправим строку кода с созданием формы следующим образом:

FForm1 := TForm1.Create(nil);

Метод TearDown вызывается по окончании тестирования и здесь нужно освободить все ресурсы и удалить все созданные объекты. Здесь сгенерированный код нас устраивает.

Метод TestCompare создан как раз для тестирования нашего метода Compare. Как видите, здесь вызывается наш метод Compare, но нет никаких проверок. Давайте добавим здесь в текстовые поля одинаковый текст, вызовем метод Compare и сделаем проверку результата.

procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
begin
   ignoreCase := true;
   //Добавляем разный текст в текстовые поля.
   FForm1.RichEdit1.Text := 'text1';
   FForm1.RichEdit2.Text := 'text2';
   //Вызываем функцию сравнения.
   FForm1.Compare(ignoreCase);
   //Проверяем результат.
   CheckEquals(1, FForm1.differentStrings.Count,
      'Сравнение не работает. Разница в текстах не определена!');
end;

После этого запустите тестовый проект Project1Tests.exe. Для этого сделайте его активным (дважды щёлкните по проекту в окошке «Project Manager») и затем запустите его выполнение (для этого выберите пункт меню «Run -> Run» или нажмите клавишу F9). После запуска перед вами появится окошко, показанное на картинке ниже.

Окно DUnit для тестирования

Чтобы запустить все тесты выберите пункт меню «Actions -> Run». Если у вас много тестов, а вам нужно выполнить только часть из них, вы можете проставить галочки рядом с нужными тестами и затем вызвать пункт меню «Actions -> Run selected test». После выполнения тестов вы увидите нашу ошибку. Это нормально, ведь пока наша функция Compare не написана и она ничего не сравнивает. Если вы щёлкните на ошибку, то в нижнем поле увидите подробности.

Проведение тестов с помощью DUnit

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

procedure TForm1.Compare(ignoreCase: boolean);
var
   i: integer;
begin
   //Очищаем список индексов разных строк.
   differentStrings.Clear;
   //Ищем разные строки.
   for i := 0 to Min(RichEdit1.Lines.Count, RichEdit2.Lines.Count) - 1 do
      if ignoreCase then
      begin
         //Сравнение строк с игнорированием регистра.
         if CompareText(RichEdit1.Lines[i], RichEdit2.Lines[i], TLocaleOptions.loUserLocale) <> 0 then
            differentStrings.Add(i);
      end
      else
      begin
         //Сравнение строк с учётом регистра.
         if CompareStr(RichEdit1.Lines[i], RichEdit2.Lines[i], TLocaleOptions.loUserLocale) <> 0 then
            differentStrings.Add(i);
      end;
   //Если в каком-то списке больше строк, чем в другом,
   //то такие строки тоже считаем разными.
   for i := Min(RichEdit1.Lines.Count, RichEdit2.Lines.Count)
         to Max(RichEdit1.Lines.Count, RichEdit2.Lines.Count) - 1 do
      differentStrings.Add(i);
end;

В методе тестирования добавим тесты для разных вариантов сравнения:

procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
begin
   ignoreCase := true;
   //Добавляем разный текст в текстовые поля.
   FForm1.RichEdit1.Text := 'text1';
   FForm1.RichEdit2.Text := 'text2';
   //Сравниваем тексты с одинаковым количеством разных строк.
   FForm1.Compare(ignoreCase);
   //Проверяем результат.
   CheckEquals(1, FForm1.differentStrings.Count,
      'Сравнение не работает. Разница в текстах не определена!');
 
   //Добавляем в первое текстовое поле ещё одну строку.
   FForm1.RichEdit1.Lines.Add('text3');
   //Сравниваем тексты с разным количеством разных строк.
   FForm1.Compare(ignoreCase);
   //Проверяем результат.
   CheckEquals(2, FForm1.differentStrings.Count,
      'Разница в текстах определена неправильно!');
 
   //Добавляем тексты с разным регистром.
   FForm1.RichEdit1.Lines.CommaText := 'Text1,text2';
   FForm1.RichEdit2.Lines.CommaText := 'text1,text2';
   //Сравниваем тексты с игнорированием регистра.
   FForm1.Compare(ignoreCase);
   //Проверяем количество различающися строк.
   CheckEquals(0, FForm1.differentStrings.Count,
      'Тексты одинаковые, а определены как разные.');
   //Сравниваем тексты с учётом регистра.
   ignoreCase := false;
   FForm1.Compare(ignoreCase);
   //Проверяем количество различающися строк.
   CheckEquals(1, FForm1.differentStrings.Count,
      'Тексты с разным регистром, а определены как одинаковые.');
   //Проверяем индекс различающихся строк.
   CheckEquals(0, FForm1.differentStrings[0],
      'Индекс различающихся строк определён неправильно.');
 
end;

Теперь протестируем нашу функцию Compare. Как видите, всё работает правильно, ошибок нет.

Тестирование DUnit завершилось успешно

Помимо метода CheckEquals в вашем распоряжении есть и другие методы для проверки результатов. Вот основные методы, которые вам могут понадобиться:

        • Check – проверяет выполнение условия и кидает исключение, если условие не выполняется.
        • CheckEquals – проверяет равенство двух значений и кидает исключение, если значения не равны.
        • CheckNotEquals – проверяет неравенство двух значений и кидает исключение, если значения равны.
        • CheckNotNull – проверяет указатель на «не nil» и кидает исключение, если указатель равен nil.
        • CheckNull – проверяет указатель на nil и кидает исключение, если указатель не равен nil.
        • CheckSame – проверяет равенство указателей на объекты и кидает исключение, если указатели не равны.
        • Fail – просто кидает исключение ETestFailure с нужным вам текстом.

Тестирование приватных методов

Отдельно хотелось бы показать, как тестировать приватные методы. Для вызова приватных методов в Delphi мы воспользуемся RTTI (Run-Time Type Information).

Для начала перенесите объявление переменной differentStrings и методов Compare и ShowDifferences в секцию private и добавьте директиву $RTTI, которая даст доступ к приватным методам (к приватным переменным доступ через RTTI разрешён по умолчанию):

{$RTTI EXPLICIT METHODS([vcPrivate..vcPublished])}
TForm1 = class(TForm)
   RichEdit1: TRichEdit;
   RichEdit2: TRichEdit;
   Button1: TButton;
   Button2: TButton;
   procedure FormCreate(Sender: TObject);
   procedure FormDestroy(Sender: TObject);
   procedure Button1Click(Sender: TObject);
   procedure Button2Click(Sender: TObject);
private
   //Список индексов разных строк.
   differentStrings: TList<integer>;
   //Процедура сравнения текста.
   procedure Compare(ignoreCase: boolean);
   //Процедура подкрашивания различающихся строк.
   procedure ShowDifferences;
public
end;

Код тестирования поменяйте следующим образом, не забыв добавить в секцию uses юнит System.Rtti:

procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
   rttiContext: TRttiContext;
begin
   ignoreCase := true;
   //Добавляем разный текст в текстовые поля.
   FForm1.RichEdit1.Text := 'text1';
   FForm1.RichEdit2.Text := 'text2';
   //Инициализируем RTTI-контекст.
   rttiContext := TRttiContext.Create;
   //Сравниваем тексты с одинаковым количеством разных строк.
   rttiContext.GetType(TForm1).GetMethod('Compare').Invoke(FForm1, [ignoreCase]);
   //Проверяем результат.
   CheckEquals(1, TList<integer>(rttiContext.GetType(TForm1).GetField('differentStrings').GetValue(FForm1).AsObject).Count,
      'Сравнение не работает. Разница в текстах не определена!');
end;

Аналогично можно тестировать приватные свойства. Директива $RTTI будет выглядеть тогда следующим образом:

{$RTTI EXPLICIT METHODS([vcPrivate..vcPublished]) PROPERTIES([vcPrivate..vcPublished])}

Итог

Итак, из статьи вы узнали, как реализована поддержка разработки с помощью тестирования в Delphi, а именно: как создавать тестовый проект, как создавать тесты и проводить тестирование, как тестировать приватные методы.

No matter how slow you are writing clean code, you will always be slower if you make a mess.
— Uncle Bob Martin

Большое спасибо всем, кто принял участие в голосовании! Судя по результатам и комментариям — тема актуальна, и многие не используют автоматическое тестирование потому, что в Интернете очень мало информации по этому вопросу. Недостаток вводных статей по тестированию кода, на мой взгляд, в том, что они очень поверхностны. Читателю предлагают сферический пример калькулятора в вакууме, который с реальными проектами никак не связан.

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

Приятного чтения!

Краткое введение в юнит-тесты

Юнит-тест — это код, который тестирует код в автоматическом режиме (т.е. без участия пользователя). Unit в этом слово-сочетании означает то, что тест проверяет только одну, выделенную часть программы (например, функцию, процедуру или метод класса) в изоляции.

Самый простой пример: Вы пишете функцию, которая из XML достает нужные Вам данные. Можно создать новый проект с одной формой и одной кнопкой, по нажатию на которую Вы загрузите XML и передадите его в свою ф-ю, после чего проверите правильность результатов (которые увидите в MessageBox’е). Или напишете процедуру, в которой заранее подготовленный XML передадите своей функции и там же, в коде, проверите ожидаемый результат. Второй способ и будет юнит-тестом.

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

Беда вводных статей по тестированию кода в том, что они описывают правила написания тестов как догму: «делайте так, только так и никак иначе, потому что так надо». Догма не гибкая и убивает креативность, а написание тестов требует гибкости и креативности. В этом случае лучше принять правило кармы: «делайте добрые дела и с вами будут случаться хорошие вещи, делайте их так, как умеете и так, как вам это нравится». Эту формулировки мысли я почерпнул из замечательной книги — The Way of Testivus (очень рекомендую к прочтению). Перевод основных ее мыслей — в конце статьи.

Нужно понимать, что идеально правильного теста не существует. Вспомните свой код, который казался Вам вершиной инженерной мысли год или два назад. Наверняка сейчас, набравшись больше опыта, Вы его таким уже не считаете. С тестами та же история — идеальный тест сегодня уже не будет таким для Вас завтра. Поэтому, не пытайтесь сделать все безупречно! Начинайте с простого! Экспериментируйте! Развивайте свой код и свои тесты итерациями, каждый раз улучшая их структуру и содержание. А если Вы сталкиваетесь с трудностями — почитайте советы опытных разработчиков, наверняка они через это уже прошли.

Также забудьте о 100% покрытии кода тестами (о котором так любят говорить задроты). Оно просто того не стоит. Здесь отлично работает правило 80/20. Всего 20% усилий сможет дать Вам проверку 80% Вашего кода (а это намного больше чем вообще ничего). К остальным 80% усилий, на мой взгляд, относится тестирование графического интерфейса. Если Вы построили приложение так, что UI используется только для передачи данных во внутрь приложения и для вывода результатов, достаточно протестировать только этот внутренний движок.

И немого информации для тех, кто считает тестирование слишком затратным (наверняка, такое ошибочное мнение складывается только из-за небольших трудностей, которые тяжело преодолеть думая о тестах как о догме). Написание теста действительно занимает больше времени, чем ручная проверка и исправление проблем через отладчик, прямо на месте. Но если посмотреть в перспективе — это очень выгодное вложение. Автоматический тест выполняется за доли секунд, ему никогда не лень и он всегда внимателен (в отличии от человека). Если одну и ту же часть программы Вы проверяете 5-7 раз вручную, уже на этом этапе тест бы себя окупил. А если подумать о регрессии (ошибка в одном месте ломает что-то в другом) тесты — незаменимая вещь.

STUBS & MOCKS

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

Stub (стаб, заглушка) — это код, которым заменяется некоторый другой код (процедура, функция, класс), от которого зависит тестируемый код. Например, Ваш метод зависит от функции, которая делает HTTP запрос. Стабом будет замена этой функции, на другую, которая возвращает заранее известный результат (и не лезет в сеть).

Mock (мок) — это тот же стаб, но который используется для того, что бы проверить, вызывал ли его Ваш код. Например, Ваш код должен сделать несколько HTTP запросов посредством метода HttpPut. Вы заменяете HttpPut на мок и в конце тестов проверяете, сколько раз он был вызван. Мок очень удобен когда Ваш код должен вызвать другую подсистему, а не вернуть какое-то значение.

Разработка приложения с использованием TDD

TDD (Test Driven Development) — это разработка приложения через тестирование. Это такой метод разработки программы, при котором тест пишется ДО ТОГО, как написан код. И в этом весь смысл! TDD заставляет нас сначала подумать об интерфейсе кода (ведь не зная интерфейс, мы не можем ничего протестировать) а уже потом — о реализации.

Если взять пример с XML, то перед написанием нашего парсера мы не лезем в Google искать «как распарсить XML в Delphi» а думаем, как эта функция будет использоваться. Ее аргумент — строка или поток (TStream)? Если XML не корректный, возвращать 0, пустой список или генерировать Exception? Правильных ответов на эти вопросы нет, выбирайте то, что Вам подходит в рамках конкретного проекта. Вся суть в том, что вы формируете ожидания от кода до написания самого кода, ведь реализацию функции заменить будет очень просто, а ее интеграцию со всей системой — намного сложнее. Далее — по очень простой итеративной схеме:

1. Пишем тест
2. Запускаем тест — он должен провалится
3. Пишем код, что бы тест заработал
4. Рефакторинг кода
5. Повторяем

Вот и все! Очень просто и одновременно очень сложно (особенно если до этого Вы тесты не писали). Главное помнить, что идеального теста нет, и улучшать тесты вместе с кодом. Конечно, есть несколько советов (я их собрал в конце статьи), но и у них есть исключения.

Пишите разные тесты для одной и той же функции, по одному на каждый вариант использования. Важно писать тест ДО написания кода. В примере с XML создайте тест с корректным и некорректным XML, с пустыми данными, с очень большими данными и т.д. Думайте о тестах не как об отдельной сущности, а как об обратной стороне монеты Вашего кода.

Коротко о DUnit

В качестве инструмента для тестирования я выбрал DUnit, т.к. он уже интегрирован в Delphi. Тесты в DUnit группируются в тест-кейсы. TestCase — это наследник класса TestFramework.TTestCase, published методы которого будут рассматриваться как независимые Unit-тесты. Но перед тем, как наш тест-кейс попадет в общий список тестов для выполнения, его нужно зарегистрировать с помощью поцедуры TestFramework.RegisterTest(…), которую удобней всего вызывать в секции initializaition. Все тест-кейсы располагаются в отдельных unit-ах и подключаются к так называемому Test Project. Он представляет собой обычный Delphi-проект, но отличается тем, что в качестве главной формы создается форма от TestFramework (также возможно создать консольный вариант). Т.о. тесты будут выполняться как отдельный проект, который может быть запущен под отладчиком (debugger, breakpoints и т.д.) или (что намного удобнее и быстрее) через меню Run — Run Without Debugging.

Таким образом, один тест представляет собой published метод TestCase’a, в котором для проверки ожидаемых результатов используются специальные функции CheckTrue, CheckEquals, CheckNull и т.д.

SHOW ME THE CODE!

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

Я сделаю иначе. Далее я поставлю себе задачу создать приложение через TDD, и опишу первый десяток итераций, т.к. считаю, что конечный результат для примера — ничто, главное — процесс и ход мыслей.

Итак, задача: написать приложение на Delphi, которое будет следить за указанным пользователем RSS-потоком, сохранять записи в локальное хранилище и показывать всплывающие подсказки на рабочем столе, если в тексте RSS-записи будут встречаться указанные пользователем слова. Приложение должно уметь сохранять свои настройки и показывать локально сохраненный список новостей.

Почему именно такой пример? Ну, во-первых, приложение может быть полезным (например, для слежением за новостями) а не абстрактным примером в вакууме. Во-вторых, в нем множество разных подсистем, их тестирование будет интересной задачей.

Подумайте, как бы такую задачу решали Вы? С чего бы начали?

Я начну с того, что приблизительно оценю сколько подсистем будет задействовано в нашем приложении. Условно разделю их на такие:

1. Взаимодействие с web
2. Разбор XML
3. Хранилище для ленты новостей
4. Основная бизнес-логика
5. Пользовательский интерфейс (UI)

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

ИТЕРАЦИЯ №1

Начнем с создания 2х проектов — первый VCL Forms Application, второй Test Project. Очень удобно их хранить вместе как Delphi Project Group. Теперь выберем одну подсистему. Я начну с первой. Проанализировав задачу приложения, видим что взаимодействие с сетью ограничивается посылкой HTTP GET запроса на некоторый URL RSS-ленты и получение в ответ XML документа. Давайте напишем тест, в котором представим как нам хотелось бы использовать эту подсистему.

Можно использовать File — New — Other — Unit Test — TestCase, в этом случае Delphi предложит сгенерировать тест-кейс для существующего файла. Но ведь пока что у нас нет тестируемого кода, в этом и смысл TDD. Поэтому добавим пустой Delphi Unit, в uses-секцию внесем модуль TestFramework и создадим каркас своего первого теста.

unit TestHttpClient;
interface

uses
  TestFramework;

type
  THttpClientTest = class(TTestCase)
  published
    procedure ItGetsContentByURL;
  end;

implementation

{ THttpClientTest }

procedure THttpClientTest.ItGetsContentByURL;
begin

end;

initialization
  RegisterTest(THttpClientTest.Suite);

end.

Уже в таком виде можно запускать TestProject. Вы увидите главное окно DUnit с кнопкой запуска тестов. Наш первый тест не проходит, т.к. мы не делали в нем никаких проверок и DUnit считает такой тест пустым. Идею называть тесты «ItGetsContentByURL» я подсмотрел в мире Ruby. На мой взгляд, это очень удобно, имя метода уже говорит нам что делает тестируемый код.

Как будет выглядеть тестируемая функция? Давайте начнем с самого простого варианта (усложнить мы всегда успеем)

procedure THttpClientTest.ItGetsContentByURL;
var Response: string;
begin
  Response := GetPageByURL('http://delphi.frantic.im/feed/');
  CheckNotEquals(0, Pos('<rss', Response), 'Response doesnt contain RSS feed');
end;

Функции GetPageByURL пока что нет, но мы уже фиксируем в коде то, какой у нее должен быть интерфейс. По URL (строка) она должна вернуть строку с содержимым ресурса, причем синхронно (блокируя вызывающий поток). После получения ресурса надо проверить что мы получили именно то, что хотели. Проверка CheckNotEquals(0, Pos(‘<rss’, Response)) — не идеальный вариант, но выполняет свою задачу. Адепты юнит-тестов увидев этот тест немедленно бросят в меня гнилой помидор — «юнит-тесты не должны быть завязаны на внешние ресурсы!». Согласен, но как протестировать HTTP-клиент не делая HTTP-запроса? Варианты из стабом всего сетевого стека или запуском локального сервера для тестов — слишком затратные.

Итак, запускаем код и видим что тест не проходит (точнее, он даже не компилируется). Исправляем это добавлением в проект RSSReader нового модуля HttpClient, и подключаем этот модуль к нашему тест-кейсу. В модуле HttpClient создаем функцию с уже определенным интерфейсом. Здесь очень важный момент, мы пишем функцию ПОСЛЕ написания теста, т.е. тест диктует нам то, какой она должна быть (test driven development).

function GetPageByURL(URL: string): string;
begin

end;

Запускаем тест-кейс, тест не проходит. Теперь можно смело гуглить на тему «как получить содержимое страници по URL в Delphi» 🙂 Я выбираю Indy, т.к. он идет в поставку вместе с Delphi, но если у вас на проекте используется Synapse или другая библиотека — можно использовать ее. Вот моя реализация:

function GetPageByURL(URL: string): string;
var
  HTTP: TIdHTTP;
  Stream: TStringStream;
begin
  HTTP := TIdHTTP.Create;
  Stream := TStringStream.Create;
  try
    HTTP.Get(URL, Stream);
    Result := Stream.DataString;
  finally
    FreeAndNil(HTTP);
    FreeAndNil(Stream);
  end;
end;

Тест проходит, можно считать первую итерацию законченной и делать коммит 🙂 a655353f5e

ИТЕРАЦИЯ №2

На этой итерации займемся парсингом XML. Следуя философии TDD сперва создадим тест (и новый тест-кейс — TestRSSParser), причем максимально простой.

procedure TRSSParserTest.ItParsesRSSFeed;
var
  FeedContent: string;
begin
  FeedContent := '';  // TODO: Load dummy feed content
  ParseRSSFeed(FeedContent, ???);
end;

И снова TDD заставляет нас подумать об интерфейсе функции перед ее реализацией. С аргументом все вполне понятно — это будет строка с XML (это удобней всего в нашем случае). А что возвращать? Посмотрев внутреннюю структуру RSS и проанализировав, что в ней интересует нас больше всего, я пришел к выводу что нам нужен класс, который будет представлять записи RSS-ленты в Delphi, т.е. некая модель.

На этом я считаю итерацию закрытой, хотя ее видимых результатов нет (как жаль что DUnit не позволяет обозначить тест как pending / ожидающий). Зато у нас расширилось представление о внутренностях нашего будущего приложения. 76926b2e18

ИТЕРАЦИЯ №3

Разработку модели начинаем с чего? Правильно, с теста!

procedure TRSSFeedModelTest.ItHasDescription;
var
  Feed: TRSSFeed;
begin
  Feed := TRSSFeed.Create;
  Feed.Description := 'Some feed';
  CheckEquals('Some feed', Feed.Description);
  FreeAndNil(Feed);
end;

Естественно, код не скомпилируется. Добавим к проекту реализацию класса TRSSFeed в модуль RssModel.

type
  TRSSFeed = class
  private
    FDescription: string;
  public
    property Description: string read FDescription write FDescription;
  end;

Аналогично первому, добавляем тесты ItHasLink и ItHasTitle. Каждый из них будет создавать новый экземпляр TRSSFeed и проверять наличие соответствующего свойства. После того, как все тесты станут «зелеными», проведем рефакторинг кода тестов. В тест-кейсе переопределим методы SetUp и TearDown. Первый будет вызван перед каждым тестом, последний — после. Очень удобно создать поле для класса TRSSFeedModelTest.FFeed: TRSSFeed и создавать/разрушать экземпляр TRSSFeed в SetUp/TearDown. Тесты будут выглядеть таким образом:

procedure TRSSFeedModelTest.ItHasDescription;
begin
  FFeed.Description := 'Some feed';
  CheckEquals('Some feed', FFeed.Description);
end;

Аналогично создаем остальные свойства модели. Конечный результат можно посмотреть в коммите: 8bc87aae24

ИТЕРАЦИЯ №4

Возвращаемся к тестам нашего парсера. Я остановился на следующей реализации теста:

procedure TRSSParserTest.ItParsesRSSFeed;
var
  FeedContent: string;
  RSSFeed: TRSSFeed;
begin
  FeedContent := IOUtils.TFile.ReadAllText('feed.xml', TEncoding.UTF8);
  RSSFeed := ParseRSSFeed(FeedContent);
  CheckEquals('Delphi Zen', RSSFeed.Title);
  // TODO: Add more checks here
end;

Я оставляю право создать экземпляр TRSSFeed функции ParseRSSFeed, т.к. в дальнейшем нам может понадобится поддержка Atom и других подвидов RSS, в результате чего наша модель данных сможет разветвится на несколько классов. У ParseRSSFeed в таком случае будет больше свободы (она сама сможет выбрать экземпляр какого из классов вернуть).

Теперь разберемся с тестовыми данными. Сохранять XML как строку или запрашивать XML при каждом вызове теста — не самая хорошая идея, поэтому мы RSS-ленту положим в файл feed.xml рядом с exe-шником тестового проекта (feed.xml надо будет добавить в систему контроля версий).

Дело за малым — распарсить XML, создать и заполнить объект класса TRSSFeed. Сначала создаем новый модуль RssParser, подключаем к проекту, добавляем в него пустую функцию ParseRSSFeed, которая возвращает nil. Запускаем тест и видим что он провалился с AV. Ок, попробуем вернуть TRSSFeed.Create. Теперь вместо AV получаем ошибку тестирования — RSSFeed.Title должен содержать строку ‘Delphi Zen’. Займемся кодом, который сделает то, что от него требуется:

uses
  XMLDoc, XMLIntf;

function ParseRSSFeed(XML: string): TRSSFeed;
var
  Doc: IXMLDocument;
begin
  Doc := LoadXMLData(XML);
  Result := TRSSFeed.Create;
  Result.Title := Doc.DocumentElement.ChildNodes.First.ChildNodes.FindNode('title').Text;
end;

Тест проходит, но надо бы его расширить, ведь нас будут интересовать все поля TRSSFeed и его Item-ы. Попробуем так:

...
  CheckEquals('Delphi Zen', RSSFeed.Title);
  CheckEquals('http://delphi.frantic.im', RSSFeed.Link);
  CheckEquals('Food for thoughts', RSSFeed.Description);
  CheckEquals(9, RSSFeed.Items.Count);
...

Далее по обычной схеме: запускаем тест — видим что он провалился — пишем код — снова запускаем тест. В TDD очень важно что бы первый раз тест провалился, иначе он бесполезен.

Мне пришлось немного повозиться с реализацией парсера из-за нескольких глупых ошибок и некоторых особенностей RSS+TXMLDocument. Но очень приятно что отдача от тестов — очень шустрая, уже через 1 секунду после изменения в коде я вижу на что она повлияла.

Если в будущем наш проект столкнется с тем, что какой-то валидный XML он не сможет распарсить, очень рационально будет сохранить его в файл и добавить новый тест в TestRSSParser тест-кейс. Т.о. можно будет посредством тестирования определить, что пошло не так и исправить ошибку в тестовом проекте. После исправления следует оставить новый тест, возможно в будущем мы внесем изменения в парсер таким образом, что именно этот тип XML снова «поломается».

По ходу реализации парсера возникает задача перевода строки формата «Mon, 06 Sep 2009 16:45:00 +0000» в дату. Сделаем это с помощью TDD: напишем сначала тест а потом реализацию.

procedure TRSSParserTest.ItParsesRSSDate;
var
  d: TDateTime;
begin
  d := ParseRSSDate('Mon, 06 Sep 2009 16:45:00 +0000');
  CheckEquals('2009-09-06 16:45:00', FormatDateTime('yyyy-mm-dd hh:nn:ss', d));
end;

Штатных средств проверки дат на равенство в DUnit, к сожалению, нет. Если сравнивать 2 даты через CheckEquals, DUnit воспримет их как 2 значения с типом double, и в случае ошибки понять чем же эти даты отличаются будет трудно. Реализацию функции ParseRSSDate можно найти в исходниках этой итерации f2755e7927.

ИТЕРАЦИЯ №5

Просмотрев тесты к предыдущим итерациям, я заметил что совершенно выпустил из виду обработку ошибок. В TDD вопрос «Как будет вести себя код, если сервер недоступен?» совершенно неверный. Корректный вариант: «Как этот код ДОЛЖЕН себя вести, если сервер недоступен?». Я хочу, что бы GetPageByURL генерировал Exception если что-то пошло не так:

procedure THttpClientTest.ItRaisesAnExceptionWhenServerIsDown;
begin
  CheckException(RequestDeadServer, EHttpClientException);
end;

procedure THttpClientTest.RequestDeadServer;
begin
  GetPageByURL('http://127.0.0.1:9919/');
end;

Добавляем описание EHttpClientException в модуль HttpClient, запускаем тест и видим что он не проходит, т.к. вместо EHttpClientException получили EIdSocketError. EIdSocketError нас не устраивает, т.к. он завязан на Indy, а значит если мы захотим написать обработчик этого exception’а, нам придется явно подключать модули Indy в код. Заменить Indy на что-то другое или обновить версию Indy в дальнейшем будет очень трудно. Аналогично создаем тест ItRaisesAnExceptionWhenServerReturnsError.

Код итерации 7928c3676b.

ИТЕРАЦИЯ №6

Теперь возьмемся за парсер RSS. Аналогично HTTP-клиенту, я хочу в случае ошибки получить Exception. Стратегия создания теста и кода подобна HttpClient, поэтому я только привожу результат итерации: ff7d418026.

ИТЕРАЦИЯ №7

А вот теперь начнется самое интересное! Я возьмусь за написание основной бизнес-логики, которая зависит от HTTP-клиента, RSS-парсера и остальных сервисов, которых пока нет. Модуль SyncManager будет заниматься скачиванием RSS-ленты, парсингом, сохранением в хранилище и отображением визуальных оповещений. Но нам надо протестировать класс, а для этого нужно иметь возможность заменить реализацию его компонентов на заглушки. Сделаем это максимально просто, никаких Dependency Injection фреймворков и XML нам не понадобится.

Покажу как это можно сделать на примере с модулем HttpClient. Нужно разбить его на интерфейс и реализацию. Перепишем модуль HttpClient следующим образом. Интерфейс (модуль HttpClient):

type
  EHttpClientException = class(Exception);

  IHttpClient = interface
    ['{CDE443FE-8249-4557-8F26-D0EC6432F274}']
    function GetPageByURL(URL: string): string;
  end;

var
  DefaultHttpClient: IHttpClient;

Реализация (модуль IndyHttpClient):

type
  TIndyHttpClient = class(TInterfacedObject, IHttpClient)
  public
    function GetPageByURL(URL: string): string;
  end;

  ...

function TIndyHttpClient.GetPageByURL(URL: string): string;

...

Соответственно нам придется немного подкорректировать тест-кейс (заменить GetPageByURL на DefaultHttpClient.GetPageByURL, написать SetUp/TearDown).

Теперь о том, как это работает. Мы отделили интерфейс модуля от его реализации. Для того, что бы использовать HttpClient в нашей программе (или тесте) надо его инициализировать, например так: DefaultHttpClient := TIndyHttpClient.Create. Эту инициализацию логично вынести в функцию, которая будет вызвана во время старта приложения (в случае с тестом — в SetUp). Т.о. весь код, который требует для работы HTTP-функционал, будет зависеть ТОЛЬКО от модуля HttpClient, а реализацию можно будет заменить в любое время позже. В тесте, например, можно будет использовать какой-нибудь TFakeHttpClient и спокойно тестировать код, который зависит от HttpClient.

Этот подход очень удобен и спасал мои проекты не раз. Особенность в том, что он очень прост — нам не надо никаких внешних библиотек или сложных XML конфигураций, код легко понять и использовать (ведь в нем практически ничего нет!).

Еще следует отметить, что изначально я не начинал проектировать модуль HttpClient таким образом. Очень важно идти от простого к сложному, иначе можно построить слишком усложненный код, который будет только мешать. Возможно, в будущем, нам понадобится Dependency Injection или еще что-то в этом роде, но на данном этапе (и на многих следующих) нет смысла усложнять код.

Предчувствую замечания о том, что глобальные переменные — это плохо. Да, плохо если у вас есть глобальный флаг Flag1 или переменная MagicVar42, но в данном случае в них есть смысл — простота. При нормальных условиях, наши экземпляры интерфейсов будут изменяться только при первом присвоении, т.о. они мало отличаются от глобальной функции или статического метода. Позже, если возникнет необходимость, можно будет улучшить это.

Подобным образом я изменил RSS-парсер, с результатами можно ознакомится в коммитах ae34faadf7 и ecc5794acd. Весь этот процесс занял у меня не более 10 минут, но на практике чтобы отделить интерфейс от реализации нужно гораздо больше времени.

ИТЕРАЦИЯ №8

Я не начинал работу над подсистемой хранилища потому что пока слабо представляю какой именно функционал будет ему необходим. В этой ситуации надо подробней изучить техническое задание на приложение, поговорить с заказчиком, продумать варианты использования и т.д. В нашем упрощенном примере, я постараюсь определить требования к хранилищу работая над SyncManager’ом. Начинаем, как всегда, с теста:

procedure TSyncManagerTest.ItGetsAndStoresNewItemsWithNotification;
begin
  Sync('http://delphi.frantic.im/feed/');
end;

В основном приложении URL будет взят с конфигурационного файла или TEdit’а. Пустая процедура Sync уже делает тест «зеленым». Самое время подумать о том, как проверить что она делает именно то что мы хотим. Sync должен запросить ресурс по HTTP, распарсить полученный RSS, сохранить его Item’ы в хранилище и показать UI-оповещение. Создадим интерфейсы недостающих служб 5bad892ac0:

type
  IRSSStorage = interface
    ['{7F674ACD-6322-4419-BCCB-D8788E75ED53}']
    procedure StoreItem(Item: TRSSItem);
  end;

var
  DefaultRSSStorage: IRSSStorage = nil;

Визуальные оповещения будут использоваться как для новых RSS-записей, так и для сообщений об ошибках:

type
  IUINotificationService = interface
    ['{A8E7E1F7-E60F-465A-9D8C-0B6186CA7A06}']
    procedure Notify(Item: TRSSItem);
    procedure NotifyError(E: Exception);
  end;

var
  DefaultUINotification: IUINotificationService = nil;

Но и этого еще недостаточно. Для запуска тестов нам надо написать заглушки для служб. Новый Delphi RTTI в комбинации со специализироваными фреймворками позволяют это делать на ходу, но мы не будем усложнять пример и сделаем все вручную. Результат можно посмотреть здесь: 3aed5be638. Принцип простой — для каждого интерфейса я создаю реализацию, которая будет вызывать заданную ей анонимную процедуру (referance to procedure/function).

Теперь можем приступить к самому важному тесту (точнее, к его первому приближению):

procedure TSyncManagerTest.ItGetsAndStoresNewItemsWithNotification;
var
  ParserWasCalled: Boolean;
begin
  // HttpClient will return some dummy XML
  FFakeHttpClient.OnGetPageByURL :=
    function (URL: string): string
    begin
      CheckEquals('http://delphi.frantic.im/feed/', URL);
      Result := 'SomeXML';
    end;

  // Parser should be called with that dummy XML
  FFakeRSSParser.OnParseRSSFeed :=
    function (XML: string): TRSSFeed
    begin
      ParserWasCalled := True;
      CheckEquals('SomeXML', XML);
    end;

  Sync('http://delphi.frantic.im/feed/');
  CheckTrue(ParserWasCalled, 'Parser should have been called');
end;

Delphi — не динамический ЯП, поэтому такие штуки, как создание и проверка моков и стабов довольно затратная (по количеству кода и сложности). Но в принципе можно понять что делает этот тест. Заглушка HTTP-клиента вернет некоторую строку, а заглушка парсера проверит, что она вызвана именно с этой строкой. Переменная ParserWasCalled нужна для того, что бы в конце теста проверить, был ли вызван парсер. Запускаем тест, получаем ошибку «Parser should have been called». Теперь самое время приняться за реализацию:

procedure Sync(URL: string);
var
  Xml: string;
begin
  Xml := DefaultHttpClient.GetPageByURL(URL);
  DefaultRSSParser.ParseRSSFeed(Xml);
end;

В TDD очень часто случается так, что код, который тестируется по объему намного меньше самого теста. И это нормально. Продолжим развивать наш тест:

...
  // Storage should receive both items from RSS
  StoredItemsCount := 0;
  FFakeRSSStorage.OnStoreItem :=
    procedure (Item: TRSSItem)
    begin
      Inc(StoredItemsCount);
    end;

  Sync('http://delphi.frantic.im/feed/');
  CheckEquals(2, StoredItemsCount, 'StoreItem should have been called 2 times');
...

Т.е. теперь мы ожидаем, что Sync(…) в конце сохранит полученные записи (которые мы создаем заранее в SetUp) Cм. полные исходники к этой итерации https://github.com/frantic/delphi-tdd-example/commit/94b92ee7915d9ba34678cd17d2377920600f0d3b.

А ЧТО ДАЛЬШЕ?

Надеюсь у меня получилось передать подход к разработке и ход мыслей. Я не буду описывать остальную часть приложения в этой статье. Полный набор исходников можно найти на Github, скачать и даже посмотреть ход развития. Код не идеален, но я стараюсь улучшать его постепенно, а не создать шедевр с первого раза.

Пример можно расширять, вводя новые службы — настройки и логирование (разумеется, вместе с тестами для них). В тестовом окружении настройки могут быть жестко заданы в коде, а в конечном приложении — в Ini/Xml/Json/YAML-файле или реестре. Разделяя интерфейс и реализацию вы значительно расширяете свои возможности и гибкость.

Больше того, если Вам понадобится перенести подобное приложение на MacOS или Linux, основная часть кода останется без изменений. Придется только заменить РЕАЛИЗАЦИИ некоторых модулей такими, что их сможет скомпилировать FreePascal на соответствующей платформе (например заменить Indy на Synapse).

Как быть со старым кодом?

Наверняка, дочитав статью до конца, у читателя может возникнуть вопрос — как внедрять описанные выше практики в уже существующем проекте? Не бросать же старый код и переписывать все с нуля? Это очень зависит от самого проекта, если компоненты системы сильно связаны между собой — задача окажется непростой. В общем — нужно создать интерфейс между подсистемами и отделить их одна от другой. Тогда некоторые подсистемы можно будет заменить заглушками и тестировать друге подсистемы станет намного проще. Разделение лучше проводить в несколько этапов, по средине пути у Вас могут существовать 2 параллельно работающие части программы (старая и новая). Удаляйте как можно больше логики с UI, в идеале там будет просто вызов внутренних функций, которые легко тестировать.

Можно не разбивать программу, а создать тесты, которые будут проверять систему вцелом. Но это не очень хороший способ, т.к. написать хороший тест в этой ситуации — очень тяжело (на большую систему влияет слишком много факторов), к тому же выполняться такие тесты будут очень медленно.

Еще одной способ — начинать писать тесты только для нового кода. Если необходимо, что бы новый код взаимодействовал со старым, выделите это взаимодействие в отдельный интерфейс.

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

Бывает и так, что в команде разработчиков к идее автоматического тестирования относятся весьма пессимистично. В таком случае Вы можете попробовать этот подход сами в отдельной ветке (Вы же используете систему контроля версий?) и потом, если эксперимент удастся, показать результаты совей команде. Очень вероятно, что на живом примере ваши коллеги изменят свою точку зрения.

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

Советы

Напомню, что советы — не догма, но во многих случаях они весьма полезны.

Советы из личного опыта:

  1. Делайте тесты «плоскими», без if, while и т.д. В тесте не должно быть логики.
  2. Запускайте тесты как можно чаще. Слишком медленные можно отключить в DUnit (если то, над чем Вы работаете, на них не влияет).
  3. Начинайте с простого. Да, в интернете есть много хитрых инструментов для тестирования, не повышайте сложность без необходимости.
  4. Если используете Continuous Integration, включите запуск консольного варианта тестового проекта в скрипт сборки.
  5. Тестирование не сделает Ваш проект успешным и не избавит от 100% багов. Но очень в этом поможет.

Советы от Testivus (вольный перевод)

  1. Если пишете код, пишите тесты
  2. Не воспринимайте тестирование как догму
  3. Воспринимайте тестирование как карму
  4. Думайте о коде и тесте как об одном целом
  5. Тестирование важней чем ограничивающие правила
  6. Лучше всего писать тесты когда код еще горячий
  7. Тесты, которые не запускаются систематично, пустая трата времени
  8. Неидеальный тест сегодня лучше, чем идеальный тест когда-то в будущем
  9. Некрасивый тест лучше чем вообще без теста
  10. Хорошие тесты проваливаются

FEEDBACK

Мне очень важно Ваше мнение. Вопросы, замечания и пожелания оставляйте в комментариях. Если информация была полезна для Вас — поделитесь с товарищами по цеху 🙂 Спасибо за чтение!

  • Авторы
  • Руководители
  • Файлы работы
  • Наградные документы

Колесникова  С.А. 1


1Муниципальное общеобразовательное учреждение “Тверской лицей”

Наумова  А.И. 1


1Муниципальное общеобразовательное учреждение “Тверской лицей”


Текст работы размещён без изображений и формул.
Полная версия работы доступна во вкладке «Файлы работы» в формате PDF

введение

В данной работе представлен материал по разработке проекта “Программа компьютерного тестирования”.

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

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

Для выполнения поставленной цели необходимо решить следующие задачи:

Для подготовки текстового файла подобрать практический материал по теме: ”Визуальный язык программирования Delphi”.

Разработать проект: “Программа компьютерного тестирования”, выполнив последовательно следующие шаги:

— создать текстовый файл;

— создать графический интерфейс;

— создать событийные процедуры;

— компилировать проект в приложение и запустить его на выполнение.

Таким образом, последовательно представлен материал по проектированию: разработка формальной и компьютерной модели с использованием языка программирования Delphi и приведены конкретные примеры компьютерного эксперимента.

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

основная часть

разработка программы компьютерного

тестирования на языке delphi

1. описание формальной модели проекта

Проект позволяет контролировать полученные знания учащихся с помощью компьютерного тестирования.

Основные характеристики проекта:

проект обеспечивает работу с тестом произвольной длины (нет ограничений на количество вопросов и ответов в тесте);

вопрос может сопровождать иллюстрацией;

для каждого вопроса может быть до 3-х возможных вариантов ответов со своей оценкой в баллах;

результат тестирования может быть отнесен к одному из 4-х уровней, например, “отлично”, ”хорошо”, ”удовлетворительно” или ”плохо”;

тест представляет собой текстовый файл, созданный в текстовом редакторе Блокнот;

программа инвариантна к различным тестам (имя файла теста указывается как параметр при вызове программы);

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

2. компьютерная модель проекта

2.1. Создать текстовый файл

Тест – последовательность вопросов и соответствующих им вариантов ответов, должен находиться в текстовом файле. Количество вопросов теста не ограничено. Программа тестирования должна получать имя файла теста из командной строки. Ниже приведен пример теста. Первая строка – это название теста. За ней следует описание 4-х уровней оценки. Для каждого уровня задается сообщение и количество правильных ответов, необходимых для его достижения. Далее следуют вопросы. Каждый вопрос представляет собой текст вопроса, за которым следуют три варианта ответа. После каждого ответа указывается количество баллов, добавляемое к общей оценке, в случае выбора этого варианта ответа (правильный ответ к общей сумме добавляет 1, за выбор неправильного — 0).

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

Файл теста состоит из 3-х разделов:

раздел заголовка;

раздел оценок;

раздел вопросов

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

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

Пример текстового файла:

Программирование на языке Delphi

Вы правильно ответили на все вопросы. Оценка — ОТЛИЧНО!

10

На некоторые вопросы Вы ответили неверно. Оценка — ХОРОШО.

8

По количеству правильных ответов оценка — УДОВЛЕТВОРИТЕЛЬНО.

6

Вы плохо подготовились к испытанию. Оценка — ПЛОХО!

5

Назначение библиотечного модуля StdCtrls

Работа с математическими функциями

0

Дополнительные свойства командной кнопки

1

Работа в графическом режиме

0

Назначение объекта Button

Кнопка для компиляции программы

0

Кнопка для запуска программы

0

Командная кнопка

1

Назначение объекта Label

Для вывода текста в текстовое окно

0

Для вывода текста в графическое окно

0

Для вывода текста на форму

1

Назначение объекта RadioButton

Переключатель

1

Список

0

Метка

0

Графический интерфейс проекта — это

Форма, на которой размещены управляющие элементы

1

Окно Инспектора объектов

0

Панель управляющих элементов

0

Что преобразует функция StrToInt()

Строковый тип в целое число

1

Строковый тип в число с плавающей точкой

0

Символьный тип в число с плавающей точкой

0

Для чего используется процедура ShowMessage()?

Для ввода данных

0

Для вывода данных

0

Для вывода окна с сообщением и командной кнопкой ОК

1

В каком текстовом редакторе необходимо создать тест?

WordPad

0

Блокнот

1

MS Word

0

Как создать исполняемый файл?

Выполнить команды: Проект – Компилировать (Project — Compile)

1

Выполнить команды: Файл — Сохранить Как…(File — Save As…)

0

Выполнить команды: Файл — Сохранить Проект Как…(File — Save Project As…)

0

Как запустить исполняемый файл?

Дважды щёлкнуть по исполняемому файлу

0

Дважды щёлкнуть по Ярлыку исполняемого файла

1

Войти в приложение Delphi и выполнить команды: Выполнить – Выполнить (Run — Run)

0

Cохранить текстовый файл с именем INFORM в отдельную папку.

2.2. Создать графический интерфейс

Поместить на форму (вкладка Standart):

командную кнопку Button1 (OK) для подтверждения выбора альтернативного ответа и перехода к следующему вопросу;

надпись Label1 (буква A) для вывода текста вопроса, начальной информации о тесте и результатов тестирования;

три надписи Label2, Label3, Label4 для вывода текста альтернативных ответов;

три переключателя RadioButton1, RadioButton2, RadioButton3 (круг) для выбора варианта ответа.

В диалоговом окне Инспектор объектов форме Form1 свойству Caption дать значение – Проверка знаний; командной кнопке Button1 свойству Caption дать значение – Дальше.

Чтобы задать значения шрифта, необходимо выделить объект, дважды щёлкнуть по сложному свойству Font со знаком “плюс”, потом щёлкнуть на командную кнопку с тремя точками. В появившемся диалогом окне для Label1: шрифт – MS Sans Serif, начертание – полужирный, размер – 12 (рис. 1); для Label2 – Label4: начертание – обычный, размер – 12; для Button1: шрифт – MS Sans Serif, начертание – обычный, размер – 12; для всех меток свойству AutoSize дать значение False, свойствам ParentFont и WordWrap – True.

Рис. 1. Для метки Label1 выбрать шрифт и задать его параметры

Окончательный вид формы разрабатываемого проекта представлен на (рис. 2).

Рис. 2. Форма разрабатываемого проекта

2.3. Создать событийные процедуры

После создания формы в окно редактора кода следует поместить описание глобальных переменных программы и процедур общего назначения. Для использования дополнительных свойств командной кнопки Button1 в раздел интерфейса (interface) после слова uses в перечень библиотечных модулей добавить модуль StdCtrls. Затем можно приступить к созданию программного кода с описанием процедур обработки событий:

procedure FormCreate(Sender: TObject);

procedure Button1Click(Sender: TObject);

procedure RadioButton1Click(Sender: TObject);

procedure RadioButton2Click(Sender: TObject);

procedure RadioButton3Click(Sender: TObject);

Программный код проекта в Приложении 1.

Сохранить проект в отдельную папку вместе с текстовым файлом INFORM. Для сохранения проекта из меню Файл (File) выбрать команду Сохранить проект как… (Save Project As…). При первом сохранении откроется диалоговое окно Сохранить модуль (Save Unit1 As). В поле Имя файла ввести значение TEST01 и щёлкнуть по кнопке Сохранить. После сохранения файла модуля проекта открывается диалоговое окно Сохранить проект (Save Project As). В поле Имя файла следует ввести имя проекта TEST.

3. компьютерный эксперимент

Запустить проект на выполнение можно как через систему объектно-ориентированного программирования Delphi, так и с помощью исполняемого файла с расширением .exe.

Запуск проекта из приложения Delphi.

После запуска Delphi из меню Файл (File) выбрать команду Открыть (Open). В открывшемся окне выбрать путь к папке с проектом, в которой находится выполняемый и текстовый файлы. Щёлкните по имени файла TEST и кнопке Открыть (Open). На экране высветится форма с управляющими элементами и программный код.

При запуске программного кода из Delphi имя файла inform.txt надо ввести в поле диалогового окна, которое доступно при выборе в меню Выполнить (Run) пункта команды Параметры (Parameters) (рис. 3).

Рис. 3. Ввод имени текстового файла

Для выполнения проекта необходимо выбрать команду Выполнить (Run) в пункте меню Выполнить (Run).

Запуск проекта с помощью исполняемого файла TEST.exe

Папку с проектом и текстовым файлом разместить на диске D:

На рабочем столе исполняемому файлу TEST.exe создать Ярлык

Щёлкнуть правой кнопкой по Ярлыку и войти в Свойства

В строке Объект через пробел рядом с исполняемым файлом TEST.exe в двойных кавычках написать имя текстового файла INFORM.txt и нажать на кнопку . Вся строка будет выглядеть следующим образом: D:DelphiTEST.exe “INFORM.txt” (рис. 4).

Рис. 4. Путь исполняемого файла в Ярлыке приложения (.exe)

Для запуска программы дважды щёлкнуть по ярлыку.

После запуска программы одним из описанных выше способов на экране появится форма с конкретным вопросом (вопрос 1) и тремя вариантами ответа. Название формы Form1 — Проверка знаний(рис. 2) меняется на Программирование на языке Delphi. Командная кнопка Button1 (Дальше) недоступна(рис. 5).

Рис. 5. Первый шаг выполнения программы

Приступить к прохождению тестирования.

Файл открывается с помощью инструкции открытия файла для чтения. После успешного открытия файла вызывается процедура, которая считывает информацию из файла и выводит её присваиванием прочитанного текста свойству Caption поля метки Label1. Затем вызывается процедура, которая считывает из файла теста информацию об уровнях оценки. Эта процедура заполняет соответствующие массивы.

Для начала тестирования и открытия командной кнопки Button1 (Дальше)выбрать один из вариантов ответа(рис. 6).

Для обработки события переключателей в программе используется общая процедура, получающая объект, на котором произошло событие. Сравнивая полученное значение с именами объектов-переключателей, процедура присваивает значение глобальной переменной для увеличения набранной суммы баллов. Кроме того эта процедура делает доступной кнопку Button1, которая после вывода очередного вопроса вновь недоступна.

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

Рис. 6. Открытие командной кнопки

Далее испытуемый последовательно отвечает на все вопросы, которые находятся в текстовом файле INFORM.txt. После выбора ответа на последний вопрос при нажатии на кнопку Дальше, на форму выводится сообщение о результатах тестирования в зависимости от набранных баллов (рис. 7 – рис. 10) и командная кнопка Button1 меняет своё название – Завершить.

Рис. 7. Количество набранных баллов соответствует оценке — ОТЛИЧНО

Рис. 8. Количество набранных баллов соответствует оценке — ХОРОШО

Рис. 9. Количество набранных баллов соответствует оценке — УДОВЛЕТВОРИТЕЛЬНО

Рис. 10. Количество набранных баллов соответствует оценке — ПЛОХО

В конце тестирования щёлкнуть по командной кнопке Завершить.

заключение

Данная программа предназначена для обобщения и систематизации имеющихся знаний у учащихся по программированию на языке Delphi. Специальная литература, а также стандартное оборудо­вание кабинета информатики являются базой для проведения контроля, который является одной из основных форм проверки и оценки знаний. Такая форма контроля за уровнем достижений в рамках предлагаемого курса строится на том основании, что выпускники должны быть включены в них субъектно. Для этого разработаны альтернативные программы по различным типам заданий.

При желании представленную разработку можно использовать по другим темам и предметам, предварительно подготовив соответствующие текстовые файлы.

Ожидаемый результат:

В итоге тестирования учащиеся самостоятельно получают возможность

Знать:

основные темы, входящие в учебный курс по программированию на языке Delphi;

Уметь:

разрабатывать проекты.

У педагогов появляется возможность проанализировать полученные результаты.

Такие работы проводились в 11-м физико-математическом классе Тверского лицея. Качество выполненных работ достаточно высокое.

приложение 1. Программный код проекта

unit TEST01;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls;

type

TForm1 = class(TForm)

Button1: TButton;

Label1: TLabel;

Label2: TLabel;

Label3: TLabel;

Label4: TLabel;

RadioButton1: TRadioButton;

RadioButton2: TRadioButton;

RadioButton3: TRadioButton;

procedure FormCreate(Sender: TObject);

procedure Button1Click(Sender: TObject);

procedure RadioButton1Click(Sender: TObject);

procedure RadioButton2Click(Sender: TObject);

procedure RadioButton3Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.dfm}

var

f: TextFile; // файл теста (вопросы и варианты ответов)

title: string; // название теста

nq: integer; // количество вопросов в тесте

right: integer; // количество правильных ответов

// количество правильных ответов, необходимых для достижения уровня

level: array[1..10] of integer;

mes: array[1..4] of string; // сообщение об оценке

buf: string;

// читает из файла вопрос, варианты ответа и выводит их в поля формы

function NextQw : boolean;

begin

if not EOF(f) then

begin

nq:= nq + 1; // счетчик общего количества вопросов

Form1.Caption := Title + ‘ — вопрос ‘ + IntToStr(nq);

// прочитать и вывести вопрос

Readln(f,buf);

Form1.Label1.Caption := buf;

// прочитать и вывести варианты ответов

// 1-й вариант

Readln(f,buf); // прочитать 1-й вариант ответа

Form1.Label2.Caption := buf;

Readln(f,buf); // оценка за выбор этого ответа (1 — правильно, 0 — нет)

Form1.RadioButton1.Tag := StrToInt(buf);

// 2-й вариант

Readln(f,buf);

Form1.Label3.Caption := buf;

Readln(f,buf);

Form1.RadioButton2.Tag := StrToInt(buf);

// 3-й вариант

Readln(f,buf);

Form1.Label4.Caption := buf;

Readln(f,buf);

Form1.RadioButton3.Tag := StrToInt(buf);

// кнопка «Дальше» не доступна, пока не выбран один из вариантов ответа

Form1.Button1.Enabled := False;

// ни один из переключателей не выбран

Form1.RadioButton1.Checked := False; Form1.RadioButton2.Checked := False;

Form1.RadioButton3.Checked := False; NextQw := TRUE;

end

else NextQw := FALSE;

end;

// событие FormCreate возникает в момент создания формы

procedure TForm1.FormCreate(Sender: TObject);

var

i: integer;

fname: string;

begin

// При запуске программного кода из Delphi имя файла inform.txt надо

// ввести в поле диалогового окна, которое доступно при выборе в меню

// Run (Выполнить) пункта команды Parameters (Параметры)

fname := ParamStr(1); // взять имя файла теста из командной строки

if fname = ‘ ‘ then

begin

ShowMessage(‘В командной строке запуска программы’ +#13+

‘надо указать файл теста.’);

Application.Terminate; // завершить программу

end;

AssignFile(f,fname);

// в процессе открытия файла возможны ошибки

try

Reset(f); // эта инструкция может вызвать ошибку

except

on EInOutError do

begin

ShowMessage(‘Ошибка обращения к файлу теста: ‘ + fname);

Application.Terminate; // завершить программу

end;

end;

// файл теста успешно открыт

// прочитать название теста — первая строка файла

Readln(f,buf);

title := buf;

// прочитать оценки и комментарии

for i:=1 to 4 do

begin

Readln(f,buf);

mes[i] := buf;

Readln(f,buf);

level[i] := StrToInt(buf);

end;

right := 0; // правильных ответов

nq := 0; // всего вопросов

NextQW; // прочитать и вывести первый вопрос

end;

// щелчок на кнопке «Дальше»

procedure TForm1.Button1Click(Sender: TObject);

var

buf: string;

i: integer;

begin

if Button1.Caption = ‘Завершить’ then Close;

// добавим оценку за выбранный вариант ответа

// оценка находится в свойстве Button.Tag

// Button.Tag = 1 — ответ правильный, 0 — нет

if RadioButton1.Checked then right := right + RadioButton1.Tag;

if RadioButton2.Checked then right := right + RadioButton2.Tag;

if RadioButton3.Checked then right := right + RadioButton3.Tag;

// вывести следующий вопрос

// NextQW читает и выводит вопрос

// NextQw = FALSE, если в файле теста вопросов больше нет

if not NextQW then

begin

// здесь значение NextQw = FALSE

Button1.Caption := ‘Завершить’;

// скрыть переключатели и поля меток

RadioButton1.Visible := False; RadioButton2.Visible := False;

RadioButton3.Visible := False; Label2.Visible := False;

Label3.Visible := False; Label4.Visible := False;

buf := ‘Тестирование завершено.’ + #13 +

‘Правильных ответов: ‘ + IntToStr(right) +

‘ из ‘ + IntToStr(nq) + ‘.’ + #13;

// выставить оценку

// right — кол-во правильных ответов

i:=1; // номер уровня

while (right < level[i]) and (i < 4) do

inc(i);

buf := buf + mes[i];

Label1.AutoSize := TRUE;

Label1.Caption := buf;

end;

end;

// щелчок на переключателе выбора первого варианта ответа

procedure TForm1.RadioButton1Click(Sender: TObject);

begin

Button1.Enabled := True; // кнопка Далее теперь доступна

end;

procedure TForm1.RadioButton2Click(Sender: TObject);

begin

Button1.Enabled := True;

end;

procedure TForm1.RadioButton3Click(Sender: TObject);

begin

Button1.Enabled := True;

end;

end.

список использованных источников и литературы

Система объектно-ориентированного программирования Delphi.

Н. Культин, Delphi в задачах и примерах, Сборник задач, Санкт-Петербург, 2008.

Н. Культин, Учебник по программированию в среде Delphi для студентов и школьников старших классов, Санкт-Петербург, 2001.

Н. Угринович, Элективный курс: Учебное пособие для учащихся старших классов информационно-технологического, физико-математического и естественно-научного профилей, Москва, 2007.

Просмотров работы: 1316

Понравилась статья? Поделить с друзьями:
  • Как написать тест на html
  • Как написать тест кейс пример
  • Как написать террария на английском
  • Как написать телеграм бот на php
  • Как написать теле2 оператору сообщение