Статьи

TDD в Python за 5 минут

Разработка, управляемая тестами, в настоящее время является базовой техникой, когда вы адаптируетесь к новому языку так же, как вы изучаете синтаксис итераций (или рекурсий) или вызовов функций. Вот мое мнение о переносе моего опыта Java и PHP TDD в Python.

Основы

Официальный интерпретатор Python поставляет модуль unittest, который вы можете использовать для замены инструментов xUnit на другие языки. Тесты, созданные для unittest — это классы, расширяющие unittest.TestCase .

По соглашению, методы, начинающиеся с * test_ * , распознаются как выполняемые тесты, а setUp () и tearDown () являются зарезервированными именами для подпрограмм, которые будут выполняться один раз для каждого теста, соответственно в начале и в конце его, как вы бы это сделали. ожидать.

Каждый из этих методов принимает в качестве параметра только self , что означает, что они будут фактически вызываться без аргументов. Вы можете делиться ссылками между методами setUp, tearDown и test_ * через self, что является эквивалентом Python.

Тем не менее, вы не обязаны определять поля в теле класса, так как вы можете назначать новые для себя в любое время. Этот пример из руководства также содержит функцию __main__ для самостоятельного запуска тестового файла, что не является обязательным, если вы используете python -m unittest .

import random
import unittest

class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = range(10)

    def test_shuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))

        # should raise an exception for an immutable sequence
        self.assertRaises(TypeError, random.shuffle, (1,2,3))

    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)

    def test_sample(self):
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)

if __name__ == '__main__':
    unittest.main()

Утверждения

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

  • assertEqual (ожидаемый, фактический) является эквивалентом assertEquals () и позволяет вам указать ожидаемое значение наряду с полученным фактическим значением. Равенство Python для объектов основано на методе __eq__.
  • assertNotEqual (notExpected, actual) является противоположностью предыдущего утверждения.
  • assertTrue (выражение) и assertFalse (выражение) позволяет создавать пользовательские утверждения; expression — это логическое значение, полученное с помощью <,>, других операторов или методов сравнения или комбинации других логических значений с и , или , или нет .
  • assertIsInstance (объект, класс) проверяет объект является экземпляром класса или подкласса.

Генерация Test Doubles, таких как Stubs и Mocks, не поддерживается по умолчанию, но есть много библиотек, которые можно интегрировать для тестирования на основе поведения.

Бег

Файлы, содержащие контрольные примеры, должны начинаться с префикса test * (например, test_tennis.py), чтобы их можно было найти автоматически:

python -m unittest discover

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

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

python -m unittest test_random
python -m unittest test_random.TestSequenceFunctions
python -m unittest test_random.TestSequenceFunctions.test_shuffle

Ката

Чтобы попробовать все эти инструменты на поле, я выполнил ката по теннису. Он состоит из реализации правил подсчета теннисного сета:

  1. Каждый игрок может получить одно из этих очков в одной игре, обозначенной как 0-15-30-40. Каждый раз, когда игрок забивает, он продвигается на одну позицию в шкале.
  2. Игрок на 40, который забивает выигрывает сет. Если …
  3. Если обоим игрокам по 40, мы находимся в * двойке *. Если игра в двойке, следующий выигравший игрок получит * преимущество *. Затем, если игрок с преимуществом набирает очки, он выигрывает, а если игрок с не преимуществом зарабатывает очки, он возвращается к двойке.

Окончательный результат (теста):

from tennis import Set, Scores
from unittest import TestCase

class TestSetWinning(TestCase):
    def test_score_grows(self):
        set = Set()
        self.assertEqual("0", set.firstScore())
        set.firstScores()
        self.assertEqual("15", set.firstScore())
        self.assertEqual("0", set.secondScore())
        set.secondScores()
        self.assertEqual("15", set.secondScore())
    def test_player_1_win_when_scores_at_40(self):
        set = Set()
        set.firstScores(3)
        self.assertEqual(None, set.winner())
        set.firstScores()
        self.assertEqual(1, set.winner())
    def test_player_2_win_when_scores_at_40(self):
        set = Set()
        set.secondScores(3)
        self.assertEqual(None, set.winner())
        set.secondScores()
        self.assertEqual(2, set.winner())
    def test_deuce_requires_1_more_than_one_ball_to_win(self):
        set = Set()
        set.firstScores(3)
        set.secondScores(3)
        set.firstScores()
        self.assertEqual(None, set.winner())
        set.firstScores()
        self.assertEqual(1, set.winner())
    def test_deuce_requires_2_more_than_one_ball_to_win(self):
        set = Set()
        set.firstScores(3)
        set.secondScores(3)
        set.secondScores()
        self.assertEqual(None, set.winner())
        set.secondScores()
        self.assertEqual(2, set.winner())
    def test_player_can_return_to_deuce_by_scoring_against_the_others_advantage(self):
        set = Set()
        set.firstScores(3)
        set.secondScores(3)
        self.assertEqual(None, set.winner())
        set.firstScores()
        set.secondScores()
        set.firstScores()
        set.secondScores()
        self.assertEqual(None, set.winner())
        self.assertEqual("40", set.firstScore())
        self.assertEqual("40", set.secondScore())

class TestScoreNames(TestCase):
    def test_score_names(self):
        scores = Scores()
        self.assertEqual("0", scores.scoreName(0))
        self.assertEqual("15", scores.scoreName(1))
        self.assertEqual("30", scores.scoreName(2))
        self.assertEqual("40", scores.scoreName(3))
        self.assertEqual("A", scores.scoreName(4))

Вывод

Вы можете проверить код (в основном процедурный, я впервые пробую это ката) на Github. После этих примеров действительно легко подобрать TDD в Python на уровне модулей, разрабатывая отдельные классы или модули. Естественная эволюция побудит нас попытаться определить сквозные тесты для целых приложений.