Полное покрытие кода

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

    Инструмент тестирования nose


    Для юнит-тестирования и сбора статистики мы используем nose. Его преимущества по сравнению с другими средствами:
    • Не надо писать дополнительный код для обвязки юнит-тестов
    • Встроенные средства для метрик, в частности для вычисления процента покрытия
    • Совместимость с Python 3 (бранч py3k на google code)

    Установка nose проблем вызвать не должна — он ставится через easy_install, есть в большинстве Linux-репозиториев или может просто устанавливаться из исходников. Для Python 3 необходимо сделать клон ветки py3k и проинсталлировать из исходников.

    Изначальный пример кода


    Тестироваться будет расчет факториала:
    #!/usr/bin/env python                                                           
    import operator

    def factorial(n):
        if n < 0:
            raise ValueError("Factorial can't be calculated for negative numbers.")
        if type(n) is float or type(n) is complex:
            raise TypeError("Factorial doesn't use Gamma function.")
        if n == 0:
            return 1
        return reduce(operator.mul, range(1, n + 1))

    if __name__ == '__main__':
        n = input('Enter the positive number: ')
        print '{0}! = {1}'.format(n, factorial(int(n)))

    Код работает только на Python 2.6 и не совместим с Python 3. Код сохранен в файле main.py.

    Юнит-тесты



    Начнем с простых тестов:
    import unittest
    from main import factorial

    class TestFactorial(unittest.TestCase):

        def test_calculation(self):
            self.assertEqual(720, factorial(6))

        def test_negative(self):
            self.assertRaises(ValueError, factorial, -1)

        def test_float(self):
            self.assertRaises(TypeError, factorial, 1.25)

        def test_zero(self):
            self.assertEqual(1, factorial(0))

    Эти тесты только проверяют функциональность. Покрытие кода — 83%:
    $ nosetests --with-coverage --cover-erase
    ....
    Name Stmts Exec Cover Missing
    -------------------------------------
    main 12 10 83% 16-17
    ----------------------------------------------------------------------
    Ran 4 tests in 0.021s

    OK

    Добавим еще один класс для стопроцентного покрытия:
    class TestMain(unittest.TestCase):

        class FakeStream:

            def __init__(self):
                self.msgs = []

            def write(self, msg):
                self.msgs.append(msg)

            def readline(self):
                return '5'

        def test_use_case(self):
            fake_stream = self.FakeStream()
            try:
                sys.stdin = sys.stdout = fake_stream
                execfile('main.py', {'__name__''__main__'})
                self.assertEqual('5! = 120', fake_stream.msgs[1])
            finally:
                sys.stdin = sys.__stdin__
                sys.stdout = sys.__stdout__

    Теперь код полностью покрыт тестами:
    $ nosetests --with-coverage --cover-erase
    .....
    Name Stmts Exec Cover Missing
    -------------------------------------
    main 12 12 100%
    ----------------------------------------------------------------------
    Ran 5 tests in 0.032s

    OK

    Выводы


    Теперь, уже на основе реального кода, можно сделать какие-то выводы:
    • Первое и самое главное — полное покрытие кода не обеспечивает полную проверку функциональности программы и не гарантирует ее работоспособность. В данном примере не было тестов для проверки комплексного типа аргумента, хотя и было обеспечено полное покрытие.
    • Полностью покрыть код можно, как минимум на Python. Да, необходимо оперировать встроенными функциями и знать как работают те или иные механизмы, но это реально, и стало еще проще в Python 3.
    • Python — динамически типизируемый язык программирования, и юнит-тестирование помогает делать проверку типов. При полном покрытии вероятность того, что типизация корректно соблюдена по всей программе, намного выше.
    • Полное покрытие помогает при изменении API используемых библиотек и при изменении самого языка программирования (см. пример для Python 3 далее). Т.к. гарантируется, что вызовется каждая строчка кода, все несоотвествия кода и API будут обнаружены.
    • И как следствие из предыдущего пункта, полное покрытие помогает тестировать код. Например, при работе на production-системе перед интеграцией софта можно провести сначала его тестирование. Зачастую нормальная отладка невозможна (скажем если нет прав на удаленной системе, и всем занимается администратор), а юнит-тесты помогут понять, где проблема.

    Адаптация под Python 3


    На примере адаптации под Python 3 я хочу показать, как полное покрытие кода помогает в работе. Итак, сначала мы просто запускаем программу под Python 3 и выдается ошибка синтаксиса:
    $ python3 main.py
    File "main.py", line 17
    print '{0}! = {1}'.format(n, factorial(int(n)))
    ^
    SyntaxError: invalid syntax

    Исправляем:
    #!/usr/bin/env python                                                                                                                                       
    import operator

    def factorial(n):
        if n < 0:
            raise ValueError("Factorial can't be calculated for negative numbers.")
        if type(n) is float or type(n) is complex:
            raise TypeError("Factorial doesn't use Gamma function.")
        if n == 0:
            return 1
        return reduce(operator.mul, range(1, n + 1))

    if __name__ == '__main__':
        n = input('Enter the positive number: ')
        print('{0}! = {1}'.format(n, factorial(int(n))))

    Теперь программу можно запускать:
    $ python3 main.py
    Enter the positive number: 0
    0! = 1

    Значит ли это, что программа рабочая? Нет! Она рабочая только до вызова reduce, что нам и показывают тесты:
    $ nosetests3
    E...E
    ======================================================================
    ERROR: test_calculation (tests.TestFactorial)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
    File "/home/nuald/workspace/factorial/tests.py", line 9, in test_calculation
    self.assertEqual(720, factorial(6))
    File "/home/nuald/workspace/factorial/main.py", line 12, in factorial
    return reduce(operator.mul, range(1, n + 1))
    NameError: global name 'reduce' is not defined

    ======================================================================
    ERROR: test_use_case (tests.TestMain)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
    File "/home/nuald/workspace/factorial/tests.py", line 38, in test_use_case
    execfile('main.py', {'__name__': '__main__'})
    NameError: global name 'execfile' is not defined

    ----------------------------------------------------------------------
    Ran 5 tests in 0.010s

    FAILED (errors=2)

    В данном примере все это можно было обнаружить и ручным тестированием. Однако на больших проектах только юнит-тестирование поможет обнаружить такого рода ошибки. И только полное покрытие кода может гарантировать что практически все несоответствия кода и API были устранены.

    Ну и собственно, рабочий код, полностью совместимый между Python 2.6 и Python 3:
    #!/usr/bin/env python                                                           
    import operator
    from functools import reduce

    def factorial(n):
        if n < 0:
            raise ValueError("Factorial can't be calculated for negative numbers.")
        if type(n) is float or type(n) is complex:
            raise TypeError("Factorial doesn't use Gamma function.")
        if n == 0:
            return 1
        return reduce(operator.mul, range(1, n + 1))

    if __name__ == '__main__':
        n = input('Enter the positive number: ')
        print('{0}! = {1}'.format(n, factorial(int(n))))


    import sys
    import unittest
    from main import factorial

    class TestFactorial(unittest.TestCase):

        def test_calculation(self):
            self.assertEqual(720, factorial(6))

        def test_negative(self):
            self.assertRaises(ValueError, factorial, -1)

        def test_float(self):
            self.assertRaises(TypeError, factorial, 1.25)

        def test_zero(self):
            self.assertEqual(1, factorial(0))

    class TestMain(unittest.TestCase):

        class FakeStream:

            def __init__(self):
                self.msgs = []

            def write(self, msg):
                self.msgs.append(msg)

            def readline(self):
                return '5'

        def test_use_case(self):
            fake_stream = self.FakeStream()
            try:
                sys.stdin = sys.stdout = fake_stream
                obj_code = compile(open('main.py').read(), 'main.py''exec')
                exec(obj_code, {'__name__''__main__'})
                self.assertEqual('5! = 120', fake_stream.msgs[1])
            finally:
                sys.stdin = sys.__stdin__
                sys.stdout = sys.__stdout__


    Тесты показывают полное покрытие и работоспособность программы под разными версиями Python:
    $ nosetests --with-coverage --cover-erase
    .....
    Name Stmts Exec Cover Missing
    -------------------------------------
    main 13 13 100%
    ----------------------------------------------------------------------
    Ran 5 tests in 0.038s

    OK
    $ nosetests3 --with-coverage --cover-erase
    .....
    Name Stmts Miss Cover Missing
    -------------------------------------
    main 13 0 100%
    ----------------------------------------------------------------------
    Ran 5 tests in 0.018s

    OK

    Заключение


    Полные покрытие кода — не панацея, которая может защитить от ошибок в программе. Однако это инструмент, который надо знать и использовать. Есть много преимуществ в полном покрытии, а недостаток по сути только один — затраты времени и ресурсов на написание тестов. Но чем больше вы будете писать тестов, тем проще они будут даваться вам в дальнейшем. В наших проектах мы уже больше года обеспечиваем стопроцентное покрытие кода, и хотя по началу было много проблем, сейчас уже покрыть полностью код совершенно не составляет проблем, т.к. отрабатаны все методики и написаны все нужные пакеты. Здесь нет никакой магии (хотя и придется работать с магией Python-а), и нужно только начать.
    P.S. Полное покрытие обладает еще одним преимуществом, которое не совсем однозначно, но несомненно важно для тех, кто считает себя профессионалом — оно заставляет лезть внутрь Python-а и понимать как он работает. Такого рода знание пригодится всем, особенно разработчикам библиотек.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 35
    • 0
      спасибо. очень познавательно. правильные выводы.
      • +6
        а почему вы проверяете
        if type(n) is float or type(n) is complex:
        а не
        if type(n) is not int:
        за этим стоит какой-то глубокий смысл?

        и почему нет теста типа такого
        def test_string(self):
           self.assertRaises(TypeError, factorial, 'kaka')
        или даже
        def test_WTF(self):
           self.assertRaises(TypeError, factorial, WRF_Class())
        это не сделало бы покрытие полней?
        • +5
          Возник такой же вопрос про строки: что метрики, используемые в статье дают не совсем корректные цифры.

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

          С другой стороны — недостаток показателен: цифра 100% вообще ни о чём не говорит, равно как и 83%. Добавленный для бюрократии кейс — тестировал лишь вызов тестируемого кода, а не сам тестируемый код, что вообще абсурдно. Что будет дальше — тесты на тесты?
          • 0
            В данном случае используется метрика от coverage-модуля. Он вычисляет покрытие из расчета строчек кода — если строка кода не была вызвана в тестах, значит она не покрыта. 100% показывает, что каждая строка была вызвана в тестах хотя бы один раз.

            «Вызов тестируемого кода» (как я понял, вы имеете в виду код, исполняемый для __main__) тоже должен тестироваться — некоторые скрипты вообще могут быть написаны без объявления функций и классов, и тем не менее содержать сложную логику, которую нужно протестировать. То, что это архитектурно неправильно, другой вопрос — есть legacy-код, который перед рефакторингом нужно покрыть тестами, иначе можно внести ошибку. А этот legacy-код может быть написан как угодно, и без декомпозиции на функции классы.
            • +8
              __main__ это клиентский код. клиентский код тестируется приёмочными и функциональными тестами, а не модульными.

              «100% показывает» — с бюрократической точки зрения, да показывает. на деле остаётся миллион вариантов, которые нужно протестировать, но вы уже успокоились, ибо 100%.

              Повторю свои выводы: эта цифра в 100% не значит абсолютно ничего, и тесты с 83% покрытием ничем не хуже, чем 100%. почему? да потому что при 83% можно покрыть вызовы со стандартными аргументами, граничными и ошибочными, а при 100 — передать одно значение и успокоиться. Вы в своей статье делаете неправильные акценты и манипулируете терминами, которые работают в маркетинге («В наших проектах мы уже больше года обеспечиваем стопроцентное покрытие кода»), но с точки зрения программистов — фразы бессмысленные и ничего не характеризующие.
              • +5
                Ах да, забыл: ваши трудности вроде «хотя и придется работать с магией Python-а» как раз и вызваны тем, что вы используете неправильные инструменты (модульные тесты, вместо функциональных).
                • –7
                  Вот как раз вы и разводите бюрократию. Кто сказал, что __main__ — это клиентский код? Может в виртуальном мире сферических программистов в вакууме весь __main__-код и является клиентским (т.е. отвечает за взаимодействие с пользователем, как я понимаю), но в реальном мире скрипт может быть вызван через exec, и иметь всю логику именно в __main__.

                  Приемочные и функциональные тесты — это тоже из области бюрократии. Разве не может быть функциональный тест оформлен в виде модульного теста? Для меня главное — обеспечить качество продукта. Если для вас главное — следовать слово-в-слово апологетам TDD, то нам разговаривать не о чем, я практик, а не теоретик, и мне нужно сдавать проекты в срок с надлежащим качеством и с минимальными расходами ресурсов.

                  100% лучше 83%, потому что как минимум я буду уверен, что при прохождении тестов на данной конфигурации большая часть кода будет работать. А иметь 17% кода, который неизвестно когда будет вызван, и когда сломается, мне совершенно не хочется, особенно когда код уже внедряется в production.
                  • +5
                    Я очень даже практик с 5+ летней практикой тестирования.

                    >> «Если для вас главное — следовать слово-в-слово апологетам TDD»
                    Это для меня не главное, более того — вы ссылаетесь на «апологетов», которых читали давно и/или плохо: как раз сильные мира сего от ТДД в один голос и твердят, что 100% покрытие (именно в логическом его смысле, когда охватываются все мыслимые и немыслимые ситуации) не нужно :-)
                    Ещё более того — как практик со стажем (ведь 5 лет это стаж?) я вам могу сказать, что у меня тесты пишутся до написания тестируемых субъектов — именно это и называется TDD. То, что вы делаете — это tests after. В случае с академическим (см. «правильным») подходом — никогда не возникнет ситуации, когда покрытие тестами (согласно выбранной ранее методике) будет отличаться от 100%.

                    И да, в то время как вы довольствуетесь кодом, который работает «по большей части» — я просто пишу код, который работает.
                    • 0
                      Я ниже писал, опыт не заменить нравоучениями ) Я лишь против чтобы такие как он молодёжь не в то русло пихали сразу =/
                      • 0
                        Почитал, эмоциональненько :-)
                    • +1
                      скажите, у вас баги бывают? ;)

                      > Разве не может быть функциональный тест оформлен в виде модульного теста
                      ваще как бы модульные тесты тестят юниты, заменяя все внешние вызовы в другие юниты на mock-объекты на момент теста. Это как бы идеальный вариант). Функциональные тесты вообще не парятся по поводу где какой юнит и что он там вызывает. Когда ломается фунциональный тест однозначно сказать что сломалось нельзя. Когда юнит-тест ломается — вариант только такой что сломался именно тестируемый юнит. In a nutshell :)

                      вы конечно можете даже через питоновский unittest в тесты засунуть даже 3d-игру. Я не понимаю что вы имеете под «оформить как».

                      А по поводу теста __main__ — ну какого хрена спорить-то? Самый правильный способ это протестировать — вызвать чертов этот скрипт факториала и передавать ему че надо строками и парсить ответ в виде буковок. И тест это будет приёмочный, т.е. тот что ИМИТИРУЕТ конечного пользователя. Пытаться проверить функцию __main__ самим питоном — это же ИДИОТИЗМ!

                    • +2
                      Абсолютно с вами согласен. Брутальное количество строк, которые были затронуты тестом — далеко не показатель качественного тестирования. Максимум, что можно твердо утверждать после 100% покрытия — что в программе нет синтаксических ошибок.

                      Грубо говоря, можно и вовсе написать:
                      self.assertRaises(ValueError, factorial, -1)
                      self.assertRaises(TypeError, factorial, 1.25)
                      self.assertEqual(1, factorial(0))

                      покрытие будет полным, но что мы на самом деле протестировали? А ничего! Даже простой «return 1» вместо непосредственно умножения пройдет этот тест.
                      • 0
                        Абсолютно верно. Меня это и смущает в статье — человек с одной стороны говорит о достигаемом высоком качестве, с другой стороны не видит очевидных недочётов даже в тестах на несчастный факториал с реализацией в 7 значащих строк кода.
                      • 0
                        Черт, отправилось раньше, не успел «self.assertEqual(1, factorial(1))» дописать туда же в листинг.
                  • 0
                    Среди целочисленных типов в Python 2.6 еще есть тип long. Но на самом деле, дополнительные проверки не нужны — Python сам выдаст TypeError при работе с объектами. Я выделил отдельный случай для float и complex, т.к. для них есть метод вычисления, но он просто не реализован в данной функции.

                    Насчет дополнительных тестов — они не изменили бы покрытие, оно и так полное. Но даже то, что оно полное, не предотвращает ошибок, и вы правильно указали, какие тесты еще можно было написать.
                    • 0
                      Питон точно так же сам выдаст и эксепшн при float. Цель тестов — зафиксировать ожидаемое поведение кода, равно как и задокументировать это поведение.
                      Т.о. тест на строку — обязателен, особенно — если вы говорите о 100%.
                      • +3
                        лучше проверяйте isinstance(n, (float, complex))
                        • 0
                          а лучше не проверяйте вообще =)
                          попытка сделать питон типизируемым — обречена на провал. Ибо как раз это и является особенностью подобных языков. Либо тогда уж интерфейсы творить надо, но кто ж мне будет запрещать симитировать этот чертов int своим объектом? Хочется строгой типизации — увы, надо тогда поменять python на тот же boo.

                          Посмотрите в библиотеки мудрых людей. Кто-нибудь проверяет isinstance(something, string) ну или что то в таком духе? Нет нет и ещё раз нет. Я должен иметь возможность засунуть в эту функцию ВСЁ что мне заблагорассудится. Если это работать не будет — это уже моя вина целиком, функция тут ну совершенно не при чём. Именно такая гибкость языка экономит профессионалам кучу времени и делает жутко нестабильными приложения у не особо крутых (пока ещё) девелоперов =)
                          • 0
                            ;) Я лишь указал как лучше в данном конкретном случае, не уходя в сторону.

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

                            Кстати, мне понравился ваш ответ, случайно работу не ищете? А то можно было бы поговорить.
                            • 0
                              Работы у меня как то никогда недостатка не было =). В данный момент кстати нахожусь в шаге ухода от своего бизнеса (ipi-manager) на работу в Москве. В общем-то и переезжаю уже вот в начале июля туда из питера ). Сложных задач в мире хватает, а я просто кипятком писаю от восторга если что то сложное попадается) Есть и обратная сторона медали — рутина меня уничтожает ;). Дон Кихот, чтож тут ещё добавить)

                              А вообще я всегда открыт к знакомствам и размышлениям. За чашечкой кофе)
                    • 0
                      А на основе каких метрик делается вывод о проценте покрытия?
                      • 0
                        «Каждый метод должен быть протистирован хотя бы одним кейсом»
                        • 0
                          • +1
                            По-моему не так, а "% строк кода, которые выполнились за время работы всех тест-кейсов".
                            Еще в coverage.py есть branch coverage, там немного по-другому.
                            • 0
                              Тогда пардон
                              На самом деле это ничего не меняет — метрика от этого не становится более говорящей.
                        • +1
                          Стремиться к полному покрытию как к отдельной ценности — вредно. Обычно, 20% тестов решают 80% проблем, а остальные 80% тестов просто обеспечивают покрытие.

                          Не знаю как в nose, а в phpUnit есть интересный отчет — пофайловое покрытие, где в каждом файле зеленым подсвечиваются строки, которые выполнялись, а белым — которые нет. Этот отчет очень помогает находить ветви логики, которые забыли покрыть тестами, и низкое покрытие в пакете/файле — индикатор, что стоит взглянуть на этот отчет.
                          • 0
                            Эх! Не видели вы в своей жизни DO-178B…
                            Счастливый Вы человек!
                          • –3
                            Для факториальчиков я могу сделать такое:

                            pastebin.com/Zxew5kh4

                            (не на питоне, естественно). По ссылке — формально верифицированный факториал (три версии) на ATS, с небольшим объяснением, что и как.

                            Давайте что-нибудь более практичное рассмотрим?
                            • +2
                              интересно, откуда ты взял слово «репозитарий»?
                              • +1
                                Лучше бы рассказали как полезно кромсать тесты на тесты интеграции, юнит тесты приёмочные тесты… уж куда полезнее чем гнаться за покрытием.

                                Второй и самый гигантский минус: 100% покрытие можно сделать только для относительно простого кода. Модуль на Си? Ой покрытие вообще не проверить. Какой-нибудь сложный препроцессинг питоновских файлов? Ой и тут не проверить. Тесты у вас 1 час отрабатывают? Поздравляю, покрытие будет считаться часов 10.

                                Как уже выше писалось выше 20% тестов решают 80% проблем. Если есть 1 большой приёмочный тест, то на нём будет видно вообще практически все возможные касяки а-ля «typo» при вызове функций (ну если например именем кто ошибся =)). Касяк лишь в том, что при этом сообщение об ошибке может быть весьма двусмысленным.И валятся такие тесты не когда что-то одно сломалось, а когда вообще хоть что-нибудь не так. И понять «что» нереально, если только специально не начать копаться уже. А вот сообщения при ошибках самих юнит-тестов будут уже четкими и понятными и будут четко говорить что сломалось. Интеграционные тесты же тестят связки разных кусочком друг с другом (чтоб не забыть все mock-объектики изменить после смены основного объекта).

                                В-третьих, пока собаку на тестах не съешь, часами отлаживая казалось бы простые вещи — не будешь понимать как писать тесты. Увы, всем надо через это пройти) Это как ходить научится.
                                • +1
                                  А, ну и да, совсем забыл =)
                                  проверьте-ка мне 100% такое вот чудо
                                  [a() for a in x if a is JopaClass]

                                  покрытие будет 100% независимо от того вызывался хоть раз a() или же ни разу.

                                  Ну или более простой касяк:
                                  if something: do something

                                  ещё веселее с разными жопками вроде

                                  {
                                  'a': a_func,
                                  'b': b_func
                                  }[func_name]()

                                  вообще в любом случае тут покрытие будет 100% если код хотя бы прочитался. А вот вызвалась ли a_func или b_func — уже загадка)
                                  • +1
                                    > Т.к. гарантируется, что вызовется каждая строчка кода, все несоотвествия кода и API будут обнаружены
                                    уберите этот бред из статьи — я могу к 3м выше ещё примеров 20 намастерить, когда в 1 строке кода гораздо больше 1 логической штуки.
                                    • +1
                                      > Полное покрытие помогает при изменении API используемых библиотек и при изменении самого языка программирования (см. пример для Python 3 далее). Т.к. гарантируется, что вызовется каждая строчка кода, все несоотвествия кода и API будут обнаружены.
                                      ещё одна бредятина. Если (куда уж без них) использовать mock-объекты, и если модуль1 вызывает модуль2, при этом модуль2 заменятся mock-объектом, то модуль1 будет вызывать всё и вся на mock-объекте но никак не на модуле2. Поменяете модуль2 — все равно все тесты будут работать. А чтобы все действительно рассыпалось в тестах интеграционные тесты и нужны. Именно они должны отвечать за соответствие API. Сказать что за соотвествие API отвечает 100% коверейдж — бред бред и ещё раз бред.

                                      Я бы на вашем месте вообще убрал фразы в которых встречается слово «гарантируется...» =).
                                      • +1
                                        Да йомайо а покрытие с метаклассами вообще бывает такое диковатое… Там и вовсе зачастую 100% не получить в принципе)
                                  • +1
                                    А ещё все так любят так любят nose. И преимущества его описывают (хотя against who?)
                                    Но мать вашу там НЕТ распараллеливания для многоядерных машин.

                                    Я терпеть не могу когда начинают что то советовать вообще не говоря об альтернативах. Тот же py.test.
                                    Учитывая что тесты отнимают зачастую больше времени чем написание самого кода. И учитывая что с течением времени «волшебно» переехать с одной системы тестирования на другую крайне затруднительно, я бы например готов был бы через месяц-два использования nose и найдя другую либу которая, оказывается, покруче будет, каждый понедельник вас бы недобрым словом вспоминал а вам бы икалось. Идиотизм.

                                    Все такие расиз**атые советчики вокруг, годик на питоне пописали и уже пишут статьи будто они прям уже коммитеры питоновские и все знают. Скажите, мол «да я просто статью написал, хочешь читай, хочешь не читай». Да, но йопт это не статья это прям учебник какой-то «так делать НАДО, а так НЕ НАДО». Есть статьи иногда, когда читаешь просто как будто беседуешь с человеком, там не грех ему спокойно объяснить что оказывается nose это далеко не единственная штука. А вам… тьфу.

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