Статьи

Написать профессиональные юнит-тесты на Python

Тестирование является основой надежной разработки программного обеспечения. Существует много типов тестирования, но наиболее важным типом является модульное тестирование. Модульное тестирование дает вам большую уверенность в том, что вы можете использовать хорошо протестированные фрагменты в качестве примитивов и полагаться на них, когда создаете их для создания своей программы. Они увеличивают ваш инвентарь доверенного кода за пределами встроенных в ваш язык и стандартной библиотеки. Кроме того, Python предоставляет отличную поддержку для написания юнит-тестов.

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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class SelfDrivingCar(object):
 
   def __init__(self):
 
       self.speed = 0
 
       self.destination = None
 
        
 
   def _accelerate(self):
 
       self.speed += 1
 
        
 
   def _decelerate(self):
 
       if self.speed > 0:
 
           self.speed -= 1
 
                    
 
   def _advance_to_destination(self):
 
       distance = self._calculate_distance_to_object_in_front()
 
       if distance < 10:
 
           self.stop()
 
 
 
       elif distance < self.speed / 2:
 
           self._decelerate()
 
       elif self.speed < self._get_speed_limit():
 
           self._accelerate()
 
    
 
   def _has_arrived(self):
 
       pass
 
        
 
   def _calculate_distance_to_object_in_front(self):
 
       pass
 
        
 
   def _get_speed_limit(self):
 
       pass
 
        
 
   def stop(self):
 
       self.speed = 0
 
        
 
   def drive(self, destination):
 
       self.destination = destination
 
       while not self._has_arrived():
 
           self._advance_to_destination()
 
 
       self.stop()
 
   def __init__(self):
 
       self.speed = 0
 
       self.destination = None
 
        
 
   def _accelerate(self):
 
       self.speed += 1
 
        
 
   def _decelerate(self):
 
       if self.speed > 0:
 
           self.speed -= 1
 
                    
 
   def _advance_to_destination(self):
 
       distance = self._calculate_distance_to_object_in_front()
 
       if distance < 10:
 
           self.stop()
 
 
 
       elif distance < self.speed / 2:
 
           self._decelerate()
 
       elif self.speed < self._get_speed_limit():
 
           self._accelerate()
 
    
 
   def _has_arrived(self):
 
       pass
 
        
 
   def _calculate_distance_to_object_in_front(self):
 
       pass
 
        
 
   def _get_speed_limit(self):
 
       pass
 
        
 
   def stop(self):
 
       self.speed = 0
 
        
 
   def drive(self, destination):
 
       self.destination = destination
 
       while not self._has_arrived():
 
           self._advance_to_destination()
 
       self.stop()

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from unittest import TestCase
 
 
 
class SelfDrivingCarTest(TestCase):
 
    def setUp(self):
 
        self.car = SelfDrivingCar()
 
         
 
    def test_stop(self):
 
        self.car.speed = 5
 
        self.car.stop()
 
        # Verify the speed is 0 after stopping
 
        self.assertEqual(0, self.car.speed)
 
         
 
        # Verify it is Ok to stop again if the car is already stopped
 
        self.car.stop()
 
        self.assertEqual(0, self.car.speed)

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

Вы должны быть дисциплинированными. Быть последовательным. Убедитесь, что тесты всегда проходят. Не позволяйте тестам сломаться, потому что вы «знаете», что код в порядке.

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

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

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

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

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

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

Модуль unittest поставляется со стандартной библиотекой Python. Он предоставляет класс TestCase , из которого вы можете получить свой класс. Затем вы можете переопределить метод setUp() чтобы подготовить тестовое устройство перед каждым тестом, и / или метод класса classSetUp() чтобы подготовить тестовое устройство для всех тестов (не сбрасывать между отдельными тестами). Существуют также соответствующие tearDown() и classTearDown() которые вы можете переопределить.

Вот соответствующие части из нашего класса SelfDrivingCarTest . Я использую только метод setUp() . Я создаю свежий экземпляр SelfDrivingCar и сохраняю его в self.car чтобы он был доступен для каждого теста.

1
2
3
4
5
6
7
8
9
from unittest import TestCase
 
 
 
class SelfDrivingCarTest(TestCase):
 
    def setUp(self):
 
        self.car = SelfDrivingCar()

Следующим шагом является написание специальных тестовых методов для тестирования тестируемого кода, в данном случае класса SelfDrivingCar который выполняет то, что должен делать. Структура метода тестирования довольно стандартна:

  • Подготовьте среду (по желанию).
  • Подготовьте ожидаемый результат.
  • Назовите тестируемый код.
  • Утверждают, что фактический результат соответствует ожидаемому результату.

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

Например, метод stop() класса SelfDrivingCar ничего не возвращает, но он изменяет внутреннее состояние, устанавливая скорость равной 0. Метод assertEqual() предоставляемый базовым классом TestCase , используется здесь для проверки того, что вызов stop() работал как ожидалось.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
def test_stop(self):
 
       self.car.speed = 5
 
       self.car.stop()
 
       # Verify the speed is 0 after stopping
 
       self.assertEqual(0, self.car.speed)
 
        
 
       # Verify it is Ok to stop again if the car is already stopped
 
       self.car.stop()
 
       self.assertEqual(0, self.car.speed)

Здесь на самом деле два теста. Первый тест — убедиться, что если скорость автомобиля равна 5 и вызывается stop() , то скорость становится равной 0. Затем еще один тест — убедиться, что ничего не происходит, если снова вызвать stop() когда автомобиль уже остановлен.

Позже я представлю еще несколько тестов для дополнительной функциональности.

Модуль doctest довольно интересный. Это позволяет вам использовать интерактивные примеры кода в вашей строке документации и проверять результаты, включая повышенные исключения.

Я не использую и не рекомендую doctest для крупных систем. Правильное юнит-тестирование требует много работы. Тестовый код обычно намного больше, чем тестируемый код. Строки документации просто не подходят для написания всесторонних тестов. Они крутые, хотя. Вот как выглядит factorial функция с doc-тестами:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
import math
 
 
 
def factorial(n):
 
    «»»Return the factorial of n, an exact integer >= 0.
 
 
 
    If the result is small enough to fit in an int, return an int.
 
    Else return a long.
 
 
 
    >>> [factorial(n) for n in range(6)]
 
    [1, 1, 2, 6, 24, 120]
 
    >>> [factorial(long(n)) for n in range(6)]
 
    [1, 1, 2, 6, 24, 120]
 
    >>> factorial(30)
 
    265252859812191058636308480000000L
 
    >>> factorial(30L)
 
    265252859812191058636308480000000L
 
    >>> factorial(-1)
 
    Traceback (most recent call last):
 
        …
 
    ValueError: n must be >= 0
 
 
 
    Factorials of floats are OK, but the float must be an exact integer:
 
    >>> factorial(30.1)
 
    Traceback (most recent call last):
 
        …
 
    ValueError: n must be exact integer
 
    >>> factorial(30.0)
 
    265252859812191058636308480000000L
 
 
 
    It must also not be ridiculously large:
 
    >>> factorial(1e100)
 
    Traceback (most recent call last):
 
        …
 
    OverflowError: n too large
 
    «»»
 
    if not n >= 0:
 
        raise ValueError(«n must be >= 0»)
 
    if math.floor(n) != n:
 
        raise ValueError(«n must be exact integer»)
 
    if n+1 == n: # catch a value like 1e300
 
        raise OverflowError(«n too large»)
 
    result = 1
 
    factor = 2
 
    while factor <= n:
 
        result *= factor
 
        factor += 1
 
    return result
 
 
 
 
 
if __name__ == «__main__»:
 
    import doctest
 
    doctest.testmod()

Как видите, строка документации намного больше, чем код функции. Это не способствует удобочитаемости.

OK. Вы написали свои юнит-тесты. Для большой системы у вас будет десятки / сотни / тысячи модулей и классов в нескольких каталогах. Как вы выполняете все эти тесты?

Модуль unittest предоставляет различные средства для группировки тестов и их программного запуска. Проверьте загрузку и запуск тестов . Но самый простой способ — это тестовое открытие. Эта опция была добавлена ​​только в Python 2.7. До 2.7 вы могли использовать нос, чтобы обнаружить и запустить тесты. У Nose есть несколько других преимуществ, таких как запуск тестовых функций без необходимости создавать класс для ваших тестовых случаев. Но для целей этой статьи давайте придерживаться unittest.

Чтобы обнаружить и запустить свои тесты, основанные на unittest, просто введите в командной строке:

python -m unittest discover

unittest просканирует все файлы и подкаталоги, запустит все найденные тесты и предоставит хороший отчет, а также время выполнения. Если вы хотите посмотреть, какие тесты он выполняет, вы можете добавить флаг -v:

python -m unittest discover -v

Есть несколько флагов, которые управляют операцией:

01
02
03
04
05
06
07
08
09
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
python -m unittest -h
 
Usage: python -m unittest [options] [tests]
 
 
 
Options:
 
  -h, —help Show this message
 
  -v, —verbose Verbose output
 
  -q, —quiet Minimal output
 
  -f, —failfast Stop on first failure
 
  -c, —catch Catch control-C and display results
 
  -b, —buffer Buffer stdout and stderr during test runs
 
 
 
Examples:
 
  python -m unittest test_module — run tests from test_module
 
  python -m unittest module.TestClass — run tests from module.TestClass
 
  python -m unittest module.Class.test_method — run specified test method
 
 
 
[tests] can be a list of any number of test modules, classes and test
 
methods.
 
 
 
Alternative Usage: python -m unittest discover [options]
 
 
 
Options:
 
  -v, —verbose Verbose output
 
  -f, —failfast Stop on first failure
 
  -c, —catch Catch control-C and display results
 
  -b, —buffer Buffer stdout and stderr during test runs
 
  -s directory Directory to start discovery (‘.’ default)
 
  -p pattern Pattern to match test files (‘test*.py’ default)
 
  -t directory Top level directory of project (default to
 
                   start directory)
 
 
 
For test discovery all test modules must be importable from the top
 
level directory of the project.

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

Если оба являются строками, он пытается преобразовать их в целые числа и добавляет их. В противном случае это вызывает исключение. Функция test_add() тестирует функцию add() с аргументами, которые являются как целыми числами, так и с аргументами, которые являются плавающими, и проверяет правильное поведение в каждом случае. Но тестовое покрытие является неполным. Случай строковых аргументов не был проверен. В результате тест проходит успешно, но опечатка в ветви, где аргументы являются обеими строками, не была обнаружена (см. Там «intg»?).

01
02
03
04
05
06
07
08
09
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
import unittest
 
 
 
def add(a, b):
 
    «»»This function adds two numbers a, b and returns their sum
 
 
 
    a and b may integers
 
    «»»
 
    if isinstance(a, int) and isinstance(b, int):
 
        return a + b
 
    elseif isinstance(a, str) and isinstance(b, str):
 
        return int(a) + intg(b)
 
    else:
 
        raise Exception(‘Invalid arguments’)
 
 
 
class Test(unittest.TestCase):
 
    def test_add(self):
 
        self.assertEqual(5, add(2, 3))
 
        self.assertEqual(15, add(-6, 21))
 
        self.assertRaises(Exception, add, 4.0, 5.0)
 
 
 
unittest.main()

Вот вывод:

01
02
03
04
05
06
07
08
09
10
11
———————————————————————-
 
Ran 1 test in 0.000s
 
 
 
OK
 
 
 
Process finished with exit code 0

Написание промышленных тестовых модулей не просто и не просто. Есть несколько вещей, которые следует учитывать, и которые необходимо сделать.

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

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

Например, наш класс SelfDrivingCar отвечает за работу машины на высоком уровне: идти, останавливаться, перемещаться. У него есть метод calculate_distance_to_object_in_front() который еще не был реализован. Эта функциональность, вероятно, должна быть реализована совершенно отдельной подсистемой. Он может включать в себя считывание данных с различных датчиков, взаимодействие с другими автомобилями с автоматическим управлением, целый стек машинного зрения для анализа изображений с нескольких камер.

Посмотрим, как это работает на практике. SelfDrivingCar примет аргумент с именем object_detector , у которого есть метод object_detector calculate_distance_to_object_in_front() , и делегирует эту функциональность этому объекту. Теперь нет необходимости в модульном тестировании, потому что object_detector отвечает (и должен быть проверен) за это. Вы все еще хотите провести модульное тестирование того факта, что вы правильно используете object_detector .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class SelfDrivingCar(object):
 
   def __init__(self, object_detector):
 
       self.object_detector
 
       self.speed = 0
 
       self.destination = None
 
                
 
   def _calculate_distance_to_object_in_front(self):
 
       return self.object_detector.calculate_distance_to_object_in_front()

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

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

С другой стороны, если одна кнопка в вашем веб-приложении на странице, которая скрыта на три уровня ниже вашей главной страницы, немного мигает, когда кто-то нажимает на нее, вы можете исправить это, но, вероятно, не добавите специальный модульный тест для этого случая. Экономика просто не оправдывает это.

Тестирование мышления важно. Один принцип, который я использую, состоит в том, что у каждого куска кода есть как минимум два пользователя: другой код, который его использует, и тест, который его тестирует. Это простое правило очень помогает с дизайном и зависимостями. Если вы помните, что вам нужно написать тест для своего кода, вы не добавите много зависимостей, которые сложно восстановить во время тестирования.

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

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

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

Сравните это с функцией, которая читает файл конфигурации XML. Ваш тест должен будет создать файл XML и передать его имя файла тестируемому коду. Ничего страшного. Но предположим, что кто-то решил, что XML отвратителен, и все файлы конфигурации должны быть в формате JSON. Они занимаются своими делами и конвертируют все файлы конфигурации в JSON. Они запускают все тесты, включая ваши тесты, и все они проходят!

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

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

Как правило, вы хотите проверить ввод в открытом интерфейсе вашего кода, потому что вы не обязательно знаете, кто будет вызывать ваш код. Давайте посмотрим на метод drive() самостоятельного вождения автомобиля. Этот метод ожидает параметр ‘destination’. Параметр ‘destination’ будет использоваться позже в навигации, но метод привода ничего не делает для проверки его правильности.

Давайте предположим, что пункт назначения должен быть кортежем широты и долготы. Существуют всевозможные тесты, которые можно проверить, чтобы убедиться, что он действителен (например, пункт назначения посреди моря). Для наших целей давайте просто убедитесь, что это кортеж с плавающей точкой в ​​диапазоне от 0,0 до 90,0 для широты и от -180,0 до 180,0 для долготы.

Вот обновленный класс SelfDrivingCar . Я реализовал тривиально некоторые из не реализованных методов, потому что метод drive() вызывает некоторые из этих методов прямо или косвенно.

01
02
03
04
05
06
07
08
09
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
69
70
71
72
73
74
75
76
77
78
79
class SelfDrivingCar(object):
 
   def __init__(self, object_detector):
 
       self.object_detector = object_detector
 
       self.speed = 0
 
       self.destination = None
 
 
 
   def _accelerate(self):
 
       self.speed += 1
 
 
 
   def _decelerate(self):
 
       if self.speed > 0:
 
           self.speed -= 1
 
 
 
   def _advance_to_destination(self):
 
       distance = self._calculate_distance_to_object_in_front()
 
       if distance < 10:
 
           self.stop()
 
 
 
       elif distance < self.speed / 2:
 
           self._decelerate()
 
       elif self.speed < self._get_speed_limit():
 
           self._accelerate()
 
 
 
   def _has_arrived(self):
 
       return True
 
 
 
   def _calculate_distance_to_object_in_front(self):
 
       return self.object_detector.calculate_distance_to_object_in_front()
 
 
 
   def _get_speed_limit(self):
 
       return 65
 
 
 
   def stop(self):
 
       self.speed = 0
 
 
 
   def drive(self, destination):
 
       self.destination = destination
 
       while not self._has_arrived():
 
           self._advance_to_destination()
 
       self.stop()

Чтобы проверить обработку ошибок в тесте, я передам недопустимые аргументы и проверю, что они правильно отклонены. Вы можете сделать это с помощью self.assertRaises() unittest.TestCase . Этот метод завершается успешно, если тестируемый код действительно вызывает исключение.

Давайте посмотрим на это в действии. Метод test_drive() передает широту и долготу за пределы допустимого диапазона и ожидает, что метод drive() вызовет исключение.

01
02
03
04
05
06
07
08
09
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
from unittest import TestCase
 
from self_driving_car import SelfDrivingCar
 
 
 
 
 
class MockObjectDetector(object):
 
    def calculate_distance_to_object_in_front(self):
 
        return 20
 
 
 
 
 
class SelfDrivingCarTest(TestCase):
 
    def setUp(self):
 
        self.car = SelfDrivingCar(MockObjectDetector())
 
 
 
    def test_stop(self):
 
        self.car.speed = 5
 
        self.car.stop()
 
        # Verify the speed is 0 after stopping
 
        self.assertEqual(0, self.car.speed)
 
 
 
        # Verify it is Ok to stop again if the car is already stopped
 
        self.car.stop()
 
        self.assertEqual(0, self.car.speed)
 
 
 
    def test_drive(self):
 
        # Valid destination
 
        self.car.drive((55.0, 66.0))
 
 
 
        # Invalid destination wrong range
 
        self.assertRaises(Exception, self.car.drive, (-55.0, 200.0))

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python -m unittest discover -v
 
test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) … FAIL
 
test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) … ok
 
 
 
======================================================================
 
FAIL: test_drive (untitled.test_self_driving_car.SelfDrivingCarTest)
 
———————————————————————-
 
Traceback (most recent call last):
 
  File «/Users/gigi/PycharmProjects/untitled/test_self_driving_car.py», line 29, in test_drive
 
    self.assertRaises(Exception, self.car.drive, (-55.0, 200.0))
 
AssertionError: Exception not raised
 
 
 
———————————————————————-
 
Ran 2 tests in 0.000s
 
 
 
FAILED (failures=1)

Чтобы исправить это, давайте обновим метод drive() чтобы фактически проверить диапазон его аргументов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
def drive(self, destination):
 
       lat, lon = destination
 
       if not (0.0 <= lat <= 90.0):
 
           raise Exception(‘Latitude out of range’)
 
       if not (-180.0 <= lon <= 180.0):
 
           raise Exception(‘Latitude out of range’)
 
        
 
       self.destination = destination
 
       while not self._has_arrived():
 
           self._advance_to_destination()
 
       self.stop()

Теперь все испытания пройдены.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
python -m unittest discover -v
 
test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) … ok
 
test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) … ok
 
 
 
———————————————————————-
 
Ran 2 tests in 0.000s
 
 
 
OK

Вы должны проверить каждую функцию и метод? В частности, стоит ли тестировать закрытые методы, вызываемые только вашим кодом? Обычно неудовлетворительный ответ: «Это зависит».

Я постараюсь быть полезным здесь и скажу вам, от чего это зависит. Вы точно знаете, кто вызывает ваш приватный метод — это ваш собственный код. Если ваши тесты для открытых методов, которые вызывают ваш приватный метод, являются всеобъемлющими, то вы уже полностью тестировали свои приватные методы. Но если приватный метод очень сложен, вы можете проверить его самостоятельно. Используйте свое суждение.

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

Вот система, которую я использую. Тесты должны быть полностью отделены от тестируемого кода (поэтому я не использую doctest). В идеале ваш код должен быть в пакете. Тесты для каждого пакета должны находиться в каталоге вашего пакета. В каталоге tests должен быть один файл для каждого модуля вашего пакета с именем test_<module name> .

Например, если в вашем пакете три модуля: module_1.py , module_2.py и module_3.py , у вас должно быть три тестовых файла: test_module_1.py , test_module_2.py и test_module_3.py в каталоге tests.

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

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