Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Единственный вопрос.. «Остановись и загорись»




Единственный вопрос.

Один тестовый случай (test case) отвечает на один вопрос о тестируемом коде. Тестовый случай должен быть способен…

  • … запускаться самостоятельно, без ввода данных от человека. Юнит тестирование должно быть автоматизировано
  • … определять самостоятельно, прошла ли тестируемая функция тест или нет, без вмешательства человека с целью интерпретировать результаты
  • … запускаться в изоляции, отдельно от остальных тестовых случаев (даже если они тестируют те же функции)

Каждый тестовый случай — это остров.

Учитывая это, давайте составим тестовый случай (тест) для первого требования: 1. Функция to_roman() должна возвращать предстставление числа в римской системе счисления для всех чисел от 1 до 3999

Не сразу ясно, как этот скрипт делает... ну, хоть что-то. Он определяет класс, не содержащий метод __init__(). Класс содержит другой метод, который никогда не вызывается. Скрипт содержит блок __main__, но тот не ссылается на класс или его методы. Но кое-что он делает, поверьте мне.

import roman1
import unittest
class KnownValues(unittest. TestCase): ①
known_values = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX')) ②
def test_to_roman_known_values(self): ③
'''to_roman should give known result with known input'''
for integer, numeral in self. known_values:
result = roman1. to_roman(integer) ④
self. assertEqual(numeral, result) ⑤
if __name__ == '__main__':
unittest. main()

① Для описания тестового случая первым делом определим класс TestCase подкласс модуля unittest. Этот класс содержит много полезных методов, которые вы можете использовать в ваших тестах для определенных условий.

② Это множество пар " число/значение", определенных мной вручную. Оно включает минимальные 10 чисел, наибольшее (3999), все числа, которые в преобразованном виде состоят из одного символа, а также набор случайных чисел. Не нужно тестировать все возможные варианты, но все уникальные варианты протестировать нужно.

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

④ Здесь и происходит вызов тестируемой функции to_roman(). (Ну, функция еще не написана, но когда будет, это будет строка, которая ее вызовет. ) Заметьте, что Вы только что определили интерфейс (API) функции to_roman(): она должна принимать число для конвертирования и возвращать строку (преставление в виде Римского числа). Если API отличается от вышеуказанного, тест вернет ошибку. Также отметьте, что Вы не отлавливаете какие-либо исключения, когда вызываете to_roman(). Это сделано специально. to_roman() не должна возвращать исключение при вызове с правильными входными параметрами и правильными значениями этих параметров. Если to_roman() выбрасывает исключение, Тест считается проваленным.

⑤ Предполагая, что функция to_roman() определена корректно, вызвана корректно, выполнилась успешно, и вернула значение, последним шагом будет проверка правильности возвращенного значения. Это общий вопрос, поэтому используем метод AssertEqual класса TestCase для проверки равенства (эквивалентности) двух значений. Если возвращенный функцией to_roman() результат (result)не равен известному значению, которое Вы ожидаете (numeral), assertEqual выбросит исключение и тест завершится с ошибкой. Если значения эквиваленты, assertEqual ничего не сделает. Если каждое значение, возвращенное to_roman() совпадет с ожидаемым известным, assertEqual никогда не выбросит исключение, а значит test_to_roman_known_values в итоге выполнится нормально, что будет означать, что функция to_roman() успешно прошла тест.

Раз у Вас есть тест, Вы можете написать саму функцию to_roman(). Во-первых, Вам необходимо написать заглушку, пустую функцию и убедиться, что тест провалится. Если тест удачен, когда функция еще ничего не делает, значит тест не работает вообще! Unit testing это как танец: тест ведет, код следует. Пишете тест, который проваливается, потом - код, пока тест не пройдет.

# roman1. py
def to_roman(n):
'''convert integer to Roman numeral'''
pass ①

① На этом этапе Вы определяете API для функции to_roman(), но пока не хотите писать ее код. (Для первой проверки теста. ) Чтобы заглушить функцию, используется зарезервированное слово Python - pass, которое... ничего не делает. Выполняем romantest1. py on в интерпретаторе для проверки теста. Если Вы вызвали скрипт с параметром -v, будет выведен подробности о работе скрипта (verbose), и Вы сможете подробно увидеть, что происходит в каждом тесте. Если повезло, увидите нечто подобное:

you@localhost: ~/diveintopython3/examples$ python3 romantest1. py -v
test_to_roman_known_values (__main__. KnownValues) ①
to_roman should give known result with known input... FAIL ②
======================================================================
FAIL: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File " romantest1. py", line 73, in test_to_roman_known_values
self. assertEqual(numeral, result)
AssertionError: 'I'! = None ③
----------------------------------------------------------------------
Ran 1 test in 0. 016s ④
FAILED (failures=1) ⑤

① Запущенный скрипт выполняет метод unittest. main(), который запускает каждый тестовый случай. Каждый тестовый случай - метод класса в romantest. py. Нет особых требований к организации этих классов; они могут быть как класс с методом для отдельного тестового случая, mfr и один класс + несколько методов для всех тестовых случаев. Необходимо лишь, чтобы каждый класс был наследником unittest. TestCase.

② Для каждого тестового случая модуль unittest выведет строку документации метода и результат - успех или провал. Как видно, тест провален.

③ Для каждого проваленного теста система выводит детальную информацию о том, что конкретно произошло. В данном случае вызов assertEqual() вызвал ошибку объявления (AssertionError), поскольку ожидалось возвращения 'I' от to_roman(1), но этого не произошло. (Если у функции нет нет явного возврата, то она вернет None, значение null в Python. )

④ После детализации каждого тестового случая, unittest отображает суммарно, сколько тестов было выполнено и сколько это заняло времени.

⑤ В целом тест считается проваленным, если хоть один случай не пройден. Unittest различает ошибки и провалы. Провал вызывает метод assertXYZ, например assertEqual или assertRaises, который провалится, если объявленное условие неверно или ожидаемое исключение не выброшено. Ошибка - это другой тип исключения, который выбрасывается тестируемым кодом или тестовым юнитом и не является ожидаемым.

Наконец, мы можем написать функцию to_roman().

roman_numeral_map = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1)) ①
def to_roman(n):
'''convert integer to Roman numeral'''
result = ''
for numeral, integer in roman_numeral_map:
while n > = integer: ②
result += numeral
n -= integer
return result

① roman_numeral_map - это кортеж кортежей, определяющий три вещи: представление базовых символов римских цифр и популярных их сочетаний; порядок римских символов (в обратном направлении, от M и до I); значения римских цифр. Каждый внутренний кортеж - это пара значений (представление, число). И это не только односимвольные римские цифры; это также пары символов типа CM (“тысяча без сотни”). Это сильно упрощает код функции to_roman().

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

Если все же не понятно, как работает функция to_roman(), добавим print() в конец цикла:

while n > = integer:
result += numeral
n -= integer
print('subtracting {0} from input, adding {1} to output'. format(integer, numeral))

Этот отладочный вывод показывает следующее:

> > > import roman1
> > > roman1. to_roman(1424)
subtracting 1000 from input, adding M to output
subtracting 400 from input, adding CD to output
subtracting 10 from input, adding X to output
subtracting 10 from input, adding X to output
subtracting 4 from input, adding IV to output
'MCDXXIV'

Ну, функция to_roman() вроде бы работает, как и предполагалось в начале главы. Но пройдет ли она написанный ранее тест?

you@localhost: ~/diveintopython3/examples$ python3 romantest1. py -v
test_to_roman_known_values (__main__. KnownValues)
to_roman should give known result with known input... ok
----------------------------------------------------------------------
Ran 1 test in 0. 016s
OK

1. Ура! Функция to_roman() прошла тест “known values”. Возможно не всесторонняя проверка, но в ее ходе проверены различные входные данные, включая числа, записываемые одним римским символом, наибольшее исходное значение (3999), и значение, дающее наибольшее римское число (3888). На этом этапе можно сказать, что функция корректно обрабатывает любые правильные исходные значения.

“Правильные” исходные значения? Хм. А как насчет неправильных?

«Остановись и загорись»

Недостаточно проверить работу функции только с правильными входными данными; также необходимо убедиться, что функция выдаст ошибку при неправильном вводе. И не просто ошибку - а такую как ожидается.

> > > import roman1
> > > roman1. to_roman(4000)
'MMMM'
> > > roman1. to_roman(5000)
'MMMMM'
> > > roman1. to_roman(9000) ①
'MMMMMMMMM'

1. Это определенно не то, что ожидалось — это не правильные римские числа! По сути, все эти числа выходят за возможные пределы, но функция все равно возвращает результат, только фиктивный. Тихое возвращение неверного значения - ооооооооооочень неверно; если возникает ошибка, лучше чтобы программа завершалась быстро и шумно. " Остановись и загорись", как говорится. " Питоновский" способ остановиться и загореться - это выбросить исключение.

Спрашивается, как же учесть это в требованиях к тестированию? Для начинающих - вот так: функция to_roman() должна выбрасывать исключение типа OutOfRangeError, если ей передать число более 3999. Как будет выглядеть тест?

class ToRomanBadInput(unittest. TestCase): ①
def test_too_large(self): ②
'''to_roman should fail with large input'''
self. assertRaises(roman2. OutOfRangeError, roman2. to_roman, 4000) ③

1. Как и в предыдущем случае, создаем класс-наследник от unittest. TestCase. У Вас может быть более одного теста на класс (как Вы увидите дальше в этой главе), но я решил создать отдельный класс для этого, потому что этот случай отличается от предыдущих. Мы поместили все тесты на " положительный выход" в одном классе, а на ошибки - в другом.

2. Как и в предыдущем случае, тест - это метод, имя которого - название теста.

3. Класс unittest. TestCase предоставляет метод assertRaises, который принимает следующие аргументы: тип ожидаемого исключения, имя тестируемой функции и аргументы этой функции. (Если тестируемая функция принимает более одного аргумента, все они передаются методу assertRaises по порядку, как будто передаете их тестируемой функции. )

Обратите особое внимание на последнюю строку кода. Вместо вызова функции to_roman() и проверки вручную того, что она выбрасывает исключение (путем обертывания ее в блок try-catch), метод assertRaises делает все это за нас. Все что Вы делаете - говорите, какой тип исключения ожидаете (roman2. OutOfRangeError), имя функции (to_roman()), и ее аргументы (4000). Метод assertRaises позаботится о вызове функции to_roman() и проверит, что она возвращает исключение roman2. OutOfRangeError.

Также заметьте, что Вы передаете функцию to_roman() как аргумент; Вы не вызываете ее и не передаете ее имя как строку. Кажись, я уже упоминал, что все в Python является объектом?

Что же происходит, когда Вы запускаете скрипт с новым тестом?

you@localhost: ~/diveintopython3/examples$ python3 romantest2. py -v
test_to_roman_known_values (__main__. KnownValues)
to_roman should give known result with known input... ok
test_too_large (__main__. ToRomanBadInput)
to_roman should fail with large input... ERROR ①
======================================================================
ERROR: to_roman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File " romantest2. py", line 78, in test_too_large
self. assertRaises(roman2. OutOfRangeError, roman2. to_roman, 4000)
AttributeError: 'module' object has no attribute 'OutOfRangeError' ②
----------------------------------------------------------------------
Ran 2 tests in 0. 000s
FAILED (errors=1)

1. Следовало ожидать этот провал (если конечно Вы не написали дополнительного кода), но… это не совсем " провал", скорее это ошибка. Это тонкое но очень важное различие. Тест может вернуть три состояния: успех, провал и ошибку. Успех, естественно, означает, что тест пройден — код делает что положено. «Провал» - то что вернул тест выше — код выполняется, но возвращает не ожидаемое значение. «Ошибка» означает, что Ваш код работает неправильно.

2. Почему код не выполняется правильно? Раскрутка стека все объясняет. Тестируемый модуль не выбрасывает исключение типа OutOfRangeError. То самое, которое мы скормили методу assertRaises(), потому что ожидаем его при вводе большого числа. Но исключение не выбрасывается, потому вызов метода assertRaises() провален. Без шансов - функция to_roman() никогда не выбросит OutOfRangeError.

Решим эту проблему - определим класс исключения OutOfRangeError в roman2. py.

class OutOfRangeError(ValueError): ①
pass ②

1. Исключения - это классы. Ошибка «out of range» это разновидность ошибки — аргумент выходит за допустимые пределы. Поэтому это исключение наследуется от исключения ValueError. Это не строго необходимо (по идее достаточно наследования от класса Exception), однако так правильно.

2. Исключение вобщем-то ничего и не делает, но Вам нужна хотя бы одна строка в классе. Встроенная функция pass ничего не делает, однако необходима для минимального определения кода в Python.

Теперь запустим тест еще раз.

you@localhost: ~/diveintopython3/examples$ python3 romantest2. py -v
test_to_roman_known_values (__main__. KnownValues)
to_roman should give known result with known input... ok
test_too_large (__main__. ToRomanBadInput)
to_roman should fail with large input... FAIL ①
======================================================================
FAIL: to_roman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File " romantest2. py", line 78, in test_too_large
self. assertRaises(roman2. OutOfRangeError, roman2. to_roman, 4000)
AssertionError: OutOfRangeError not raised by to_roman ②
----------------------------------------------------------------------
Ran 2 tests in 0. 016s
FAILED (failures=1)

1. Тест по-прежнему не проходит, хотя уже и не выдает ошибку. Это прогресс! Значит, метод assertRaises() был выполнен и тест функции to_roman() был произведен.

2. Конечно, функция to_roman() не выбрасывает только что определенное исключение OutOfRangeError, так как Вы ее еще " не заставили". И это хорошие новости! Значит, тест работает, а проваливаться он будет, пока Вы не напишете условие его успешного прохождения.

Этим и займемся.

def to_roman(n):
'''convert integer to Roman numeral'''
if n > 3999:
raise OutOfRangeError('number out of range (must be less than 4000)') ①
result = ''
for numeral, integer in roman_numeral_map:
while n > = integer:
result += numeral
n -= integer
return result

1. Все просто: если переданный параметр больше 3999, выбрасываем исключение OutOfRangeError. Тест не ищет текстовую строку, объясняющую причину исключения, хотя Вы можете написать тест для проверки этого (но учтите трудности, связанные с различными языками - длина строк или окружение могут отличаться).

Позволит ли это пройти тест? Узнаем:

you@localhost: ~/diveintopython3/examples$ python3 romantest2. py -v
test_to_roman_known_values (__main__. KnownValues)
to_roman should give known result with known input... ok
test_too_large (__main__. ToRomanBadInput)
to_roman should fail with large input... ok ①
----------------------------------------------------------------------
Ran 2 tests in 0. 000s
OK

1. Ура! Оба теста пройдены. Так как Вы работали, переключаясь между кодированием и тестированием, то Вы с уверенностью можете сказать, что именно последние 2 строки кода позволили тесту вернуть " успех", а не " провал". Такая уверенность далась не дешево, но окупит себя с лихвой в дальнейшем.

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...