Решение экзаменационных задач на классы эквивалентности

advertisement
Лекция 03
Классы эквивалентности
Для автоматизации тестирования используются классы
Существуют несколько подходов выделения классов эквивалентности.
эквивалентности.
Функциональный подход
Первый – простой. Пусть, к примеру, функция принимает на вход один
действительный аргумент. Можно выделить 5 классов эквивалентности вещественных чисел:
3 граничных (2 около крайних значений и одно – около 0) и 2 промежуточных
(положительные и отрицательные, например). Нуль – число магическое. Вблизи него
программы ведут себя зачастую достаточно странно, хотя научно это не объяснено
(возможно, это связано с тем, что вблизи нуля точность представления вещественных
чисел резко возрастает).
Стоит ли разбивать на классы эквивалентности пространство выходных значений?
Научно обоснованного ответа на этот вопрос нет, но так делают. На принадлежность классу
эквивалентности проверять легко, но управлять попаданием в тот или иной класс достаточно
сложно. Если это все-таки удается, то практика показывает, что если проверить все классы
входных и выходных доменов, а также проверить все переходы между этими классами, то
получится довольно хорошее тестирование.
Подобный метод называется методом функциональных диаграмм. Но возникает
вопрос: как подобную задачу поставить практическому инженеру (то есть, программисту)?
Программист достаточно просто сможет разбить входные значения на классы
эквивалентности: граничные точки – классы эквивалентности, сингулярности – классы
эквивалентности, все, что между ними – тоже отдельные классы эквивалентности.
Аналогично делается и с выходными классами эквивалентности. А как можно определить
возможные переходы между этими классами?
Часть информации можно почерпнуть из спецификаций, но только часть. Пусть,
например, имеется явная или неявная спецификация:


if a > b then … elseif 2 * a > b then … else … end
post if a > b then … end
Имеются некоторые формулы, на основе которых можно произвести разбиение
входных и выходных доменов. Но структура переходов остается сложной. Поэтому вывод:
функциональные диаграммы применять очень сложно, если они не даны явно. Они
могут быть даны явно, если требуются, например, в проектной документации.
Построение на основе Control Flow
Подходов к построению классов эквивалентности несколько, но они не являются
взаимоисключающими. Их можно (и нужно) сочетать.
Пусть имеется набор функций, и каждая из них представлена (или представима) в
виде блок-схемы. Можно считать, что каждый из путей на этом графе (он характеризуется
входными данными и поведением окружения) соответствует некоторому классу
эквивалентности. На доске нарисован граф с четырьмя путями. Как из путей на графе
построить классы эквивалентности?
Можно идти по каждому пути, собирая ограничения. Эти ограничения дают
некоторое разбиение входных данных.
Нацеливаясь на покрытие всех путей, мы неявно предполагаем, что в программе
имеется некоторая ошибка, которая локализована на некотором из путей. Рано или поздно
мы попытаемся выполнить ошибочное действие, и поймаем эту ситуацию, и тогда
тестирование даст положительный результат. Гипотеза состоит именно в том, что ошибка
связана с одной точкой в пути программе. Всегда ли это верно? Нет. Мы не найдем (как
минимум) ошибки, которая состоит в том, что некоторая возможность в принципе не
реализована. Ошибки могут быть не только в ошибочном операторе, но и в отсутствии
операторов. А где тот эталон, который это определяет? Этот эталон – проектная
документация. Там где-то явно (а где-то – между строк) указано, что такая возможность
существует.
Вне зависимости от того, какого формата документ – текст, электронная таблица или
код – можно всегда мыслить построение классов эквивалентности на покрытии путей.





Требования полноты (уровни):
Все операции проверены (под операциями понимаются, вообще говоря, функции)
Все операторы проверены
Все ветви проверены (это отличается от проверки всех операторов; ветви могут не
содержать операторов; но если добавить во второй пункт требование проверки пустых
операторов, то они будут эквивалентны)
Все подпути длины 2 проверены (для любого узла потребовать, чтобы были покрыты
любые пары входных и выходных ребер)
Все подпути длины n проверены (редко для больших значений n)
Построение на основе Data Flow
Сначала немного о самой модели. Во всех программах где-то есть операторы
присваивания. Они изменяют состояние программы. Левая часть оператора присваивания –
точка создания нового значения некоторой переменной. Все, что находится в правой части
(если нет побочного эффекта) – использование того, что уже было в программе до этого, без
создания новых значений.
Всю программу можно рассмотреть как граф точек появления новых значений.
Дугами же будут связи по использованию.
x
z
y
x=y+z+x
Пунктирные стрелки показывают, что зависимость может быть как от одного
значения, так и от другого – все зависит от того, какой оператор будет выполнен.
Из опыта известно:
1. Чтобы добиться качества тестирования, нужно сочетать несколько методов
тестирования
2. Если вы проводили тестирование и покрыли все классы эквивалентности, вам
обязательно зададут вопрос о покрытии операторов или ветвей
Решение экзаменационных задач на классы эквивалентности
Задача: написать классы эквивалентности, которые строятся по спецификации на
основе постусловия.
Считается, что постусловие представляет собой условную конструкцию. Конструкция
ветвей везде одинакова, однако условия будут различаться.
Пример:
post if c1 then … elseif c2 then … else … end
Ветвь 1: c1 = true
Ветвь 2: c1 = false, c2 = true
Ветвь 3: c1 = false, c2 = false
Вспомним немного классическую логику. Тело постусловия всегда является
булевской функцией. Если имеется функция от 2-х переменных, то можно рассматривать
такие ситуации: c1 & c2, c1 & ~c2, ~c1 & c2, ~c1 & ~c2. Любая логическая функция
представляется в виде совершенной ДНФ (full disjunctive normal form, FDNF), в нашем
случае это будет полная СДНФ от 2-х переменных: c1 & c2 | c1 & ~c2 | ~c1 & c2 | ~c1 & ~c2.
Однако, с точки зрения программиста, когда мы вычисляем c1, мы можем еще не знать
значения c2. Логика должна не диктовать правила, а подсказывать решения. Она
представляет собой хороший инструмент анализа логических значений. Но классическая
логика нас ограничивает. В программировании могут быть невычислимые компоненты,
в логике – нет.
Увидев условное выражение выше, мы выделим такие классы по 2-м причинам: вопервых, от c2 в первом случае ничего не зависит, во-вторых, оно может быть просто
невычислимым. Поэтому разбиение на классы такое: с1 | ~c1 & c2 | ~c1 & ~c2.
На экзамене условные выражения будут намного сложнее. Например:
c1 = (x > 0) /\ (b in dom m)
c2 = (len l > k) /\ (l(k) ~= 0)
Если c1 и c2 – составные, то используются дополнительные правила. Пусть
выражения имеют вид c1 = a1 /\ a2, c2 = b1 \/ b2. Тогда выражения для ветвей имеют вид:
Ветвь 1: a1 /\ a2
Ветвь 2: ~(a1 /\ a2) /\ (b1 \/ b2)
Ветвь 3: ~(a1 /\ a2) /\ ~(b1 \/ b2).
Далее – все, как в классической логике. Раскрываем скобки (по короткой логике):
Ветвь 2: (~a1 \/ ~a2)  (~a1 \/ a1 & ~a2), ~a1 & b1 \/ ~a1 & ~b1 & b2 \/ a1 & ~a2 & b1 \/
a1 & ~a2 & ~b1 & b2. Поскольку мы работали с короткой логикой, то мы гарантируем, что
все выражения вычислимы. Также следует запомнить, что порядок вычислений менять
нельзя.
Теперь переходим к тому, что делать на экзамене. Во всех примерах будет
предусловие. В нем тоже будет определенное количество логических термов. Пример
задания:
f(x, y) …
post if t3 \/ t4 then … elseif t5 /\ t6 then … else … end.
pre t1 /\ t2
Термы нумеровать самостоятельно. Важно начинать нумерацию с термов
предусловия.
На основе постусловия пишется таблица (ветвь повторяется столько раз, сколько она
содержит классов эквивалентности, примеры – в нотации RSL):
термы
№ ветви
1
1
2
3
3
M1 (x M2
> 0)
T
T
T
T
T
T
T
T
T
T
T
T
T
T
M3
T
F
F
F
F
M4
T
F
F
F
М5
T
F
F
M6
(T)
F
Конъюнкты,
соответствующие
классу экв.
Пример
x
y
m1 & m2 & m3
X1
X2
Y1
Y2
m1 & m2 & ~m3 &
~m4 & m5 & ~m6
…
Сначала раскрываются выражения, в соответствии с короткой логикой, как выше.
Когда подбираются примеры, зачастую находятся ошибки в таблице. Поэтому
сначала все нужно делать на черновике. Дома, чтобы набить руку, следует решить как
минимум 10 задач. Кроме того, для некоторых классов эквивалентности мы не найдем
примеров. Поскольку при построении термов мы не учитывали их внутренней
структуры, термы могут оказаться противоречивыми. Например (в нотации RSL – А. В.)
a isin s /\ card s = 0 не может быть истинным. Или (рассматриваем нашу таблицу), если
шестой терм – (x < 0), а первый – (x > 0), то класс ветви 2 невыполним. Его следует
зачеркнуть. Таблицу переписывать не надо – просто зачеркнуть класс прямо в таблице.
Алгоритм решения задачи
1. Перенумеровать все ветки (структура постусловия всегда будет иметь фиксированный
вид – if … elseif … else … end, и ветки всегда будет 3).
2. Перенумеровать все термы. Начинать нумерацию следует с термов предусловий. Если
будут встречаться термы явно противоположного содержания (например, a > c и a <=
c или a isin b и a ~isin b), то выражение можно немного изменить, подставив вместо
второго терма отрицание первого. Это упросит задачу, так как количество термов
сократится (если этого не сделать, то соответствующие классы эквивалентности все
равно будут отброшены на последнем шаге – когда будут подбираться примеры).
3. Для каждой ветви (в порядке ветвей) выписать все классы эквивалентности. Под
классом эквивалентности понимается комбинация значений термов. Следует
помнить, что:
a. Предусловие всегда истинно.
b. Используется короткая логика (то есть, например, если m1 \/ m2 – дизъюнкция
термов и m1 = true, то значения m2 не определено; однако оно может стать
определенным, если это потребуется в одном из следующих условий).
4. Для каждого класса эквивалентности выписывается конъюнкция термов, ему
соответствующая.
5. Для каждого класса эквивалентности, на основании этой конъюнкции термов,
строится пример значений входных данных из этого класса. Если пример построить
не удается, то соответствующий класс вычеркивается из таблицы.
Пример решения задачи
Пример взят из книги «Формальная спецификация программ на языке RSL», страница
53.
Задача:
value
f : Int >< Int-list >< Nat -~-> Nat >< Bool f (a,b,c) as (q,p)
post
if (a + len b > c) /\ (a isin elems b) then ...
elsif (a>=c) /\ (a isin inds b) then ...
else …
end
pre (a < c) \/ (a isin inds b)
Перепишем противоположные термы и занумеруем ветви:
value
f : Int >< Int-list >< Nat -~-> Nat >< Bool f (a,b,c) as (q,p)
post
if (a + len b > c) /\ (a isin elems b) then
... (1)
elsif ~(a < c) /\ (a isin inds b) then
... (2)
else
… (3)
end
pre (a < c) \/ (a isin inds b)
Выпишем термы:
m1 = a < c
m2 = a isin inds b
m3 = a + len b > c
m4 = a isin elems b
Записываем таблицу и заполняем ее (я выписываю таблицу два раза, чтобы показать порядок
заполнения). Сначала – классы эквивалентности без примеров:
Ветвь №
1
1
2
2
3
3
m1
T
F
F
F
T
T
m2
T
T
T
-
m3
T
T
F
T
F
T
m4
T
T
F
F
Конъюнкции
m1 /\ m3 /\ m4
~m1 /\ m2 /\ m3 /\ m4
~m1 /\ m2 /\ ~m3
~m1 /\ m2 /\ m3 /\ ~m4
m1 /\ ~m3
m1 /\ m3 /\ ~m4
Пример
Теперь – с примерами:
Ветвь
№
1
1
2
2
3
3
m1 m2 m3 m4
T
F
F
F
T
T
T
T
T
-
T
T
F
T
F
T
T
T
F
F
Конъюнкции
Пример
m1 /\ m3 /\ m4
~m1 /\ m2 /\ m3 /\ m4
~m1 /\ m2 /\ ~m3
~m1 /\ m2 /\ m3 /\ ~m4
m1 /\ ~m3
m1 /\ m3 /\ ~m4
(0, <. 0, 1, 2 .>, 1)
(1, <. 0, 1, 2 .>, 1)
(1, <. 0, 2, 4.>, 1)
(1, <. .>, 10)
(1, <. 0, 2, 4 .>, 2)
Строим примеры:
1. Ветвь 1-1: a < c /\ a + len b > c /\ a isin elems b. Пример: (0, <. 0, 1, 2 .>, 1)
2. Ветвь 1-2: a >= c /\ a isin inds b /\ a + len b > c /\ a isin elems b. Пример: (1, <. 0, 1, 2 .>,
1).
3. Ветвь 2-1: a >= c /\ a isin inds b /\ a + len b <= c. Условие является противоречивым,
так как первый и третий термы этого выражения могут быть удовлетворены
одновременно только при a = c и b = <. .>, но тогда второй терм будет ложным.
4. Ветвь 2-2: a >= c /\ a isin inds b /\ a + len b > c /\ a ~isin elems b. Пример: (1, <. 0, 2, 4.>,
1).
5. Ветвь 3-1: a < c /\ a + len b <= c. Пример: (1, <. .>, 10).
6. Ветвь 3-2: a < c /\ a + len b > c /\ a ~isin elems b. Пример: (1, <. 0, 2, 4 .>, 2).
Резюме
1. Имеется ясный, систематический подход, так что любой программист с задачей
справится
2. Можно ли построить инструмент? Можно, если термы, во-первых, не имеют скрытой
функциональной зависимости, и, во-вторых, нет скрытой информации. Если второе не
выполнено, то произвести разбивку на классы эквивалентности можно, но указать
достижимость нельзя. Однако если мы построили классы эквивалентности, то
тестирование уже можно автоматизировать. А если есть постусловия, то, в принципе,
решена задача построения оракула.
Последние 2 вопроса:
1. Можно ли при помощи классов эквивалентности тестировать математические
функции? Можно, но не ясно, какова будет эффективность. Наверняка придется часть
ветвей ввести руками.
2. Можно ли при помощи классов эквивалентности тестировать программы,
работающие с файлами? Реальные спецификации будут рассматривать много
возможностей, они будут достаточно сложными. Для тестового покрытия требуется
генерация наборов входных данных. Возникает задача получения входных данных
довольно сложной структуры.
Download