Как запустить юниты вида test / a.py? - PullRequest
0 голосов
/ 06 мая 2018

Возможно ли реализовать проект Python с файловой структурой, подобной следующей?

myproj
├── a.py
├── b.py
├── c.py
└── test/
    ├── a.py
    ├── b.py
    └── c.py

Обратите внимание, в частности, что сценарии тестирования в test/ имеют те же базовые имена, что и файлы модулей, которые они проверяют 1 . (Другими словами, test/a.py содержит модульные тесты для a.py; test/b.py содержит тесты для b.py и т. Д.)

Тесты по test/ все импортируют unittest и определяют подклассы unittest.TestCase.

Я хочу знать, как запускать тесты под test/, как по отдельности, так и все вместе.

Я перепробовал множество вариантов python -m unittest ..., но все они либо терпят неудачу (примеры ниже), либо заканчивают тем, что запускают нулевые тесты.

Например,

% python -m unittest test.a
Traceback (most recent call last):
  File "/usr/lib/python2.7/runpy.py", line 174, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/usr/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/usr/lib/python2.7/unittest/__main__.py", line 12, in <module>
    main(module=None)
  File "/usr/lib/python2.7/unittest/main.py", line 94, in __init__
    self.parseArgs(argv)
  File "/usr/lib/python2.7/unittest/main.py", line 149, in parseArgs
    self.createTests()
  File "/usr/lib/python2.7/unittest/main.py", line 158, in createTests
    self.module)
  File "/usr/lib/python2.7/unittest/loader.py", line 130, in loadTestsFromNames
    suites = [self.loadTestsFromName(name, module) for name in names]
  File "/usr/lib/python2.7/unittest/loader.py", line 100, in loadTestsFromName
    parent, obj = obj, getattr(obj, part)
AttributeError: 'module' object has no attribute 'a'

Если я изменю имя каталога test/ на t/, то ошибка будет выглядеть так:

% python -m unittest t.a
Traceback (most recent call last):
  File "/usr/lib/python2.7/runpy.py", line 174, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/usr/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/usr/lib/python2.7/unittest/__main__.py", line 12, in <module>
    main(module=None)
  File "/usr/lib/python2.7/unittest/main.py", line 94, in __init__
    self.parseArgs(argv)
  File "/usr/lib/python2.7/unittest/main.py", line 149, in parseArgs
    self.createTests()
  File "/usr/lib/python2.7/unittest/main.py", line 158, in createTests
    self.module)
  File "/usr/lib/python2.7/unittest/loader.py", line 130, in loadTestsFromNames
    suites = [self.loadTestsFromName(name, module) for name in names]
  File "/usr/lib/python2.7/unittest/loader.py", line 91, in loadTestsFromName
    module = __import__('.'.join(parts_copy))
ImportError: No module named t

Или

% python -m unittest t/a.py
Traceback (most recent call last):
  File "/usr/lib/python2.7/runpy.py", line 174, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/usr/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/usr/lib/python2.7/unittest/__main__.py", line 12, in <module>
    main(module=None)
  File "/usr/lib/python2.7/unittest/main.py", line 94, in __init__
    self.parseArgs(argv)
  File "/usr/lib/python2.7/unittest/main.py", line 149, in parseArgs
    self.createTests()
  File "/usr/lib/python2.7/unittest/main.py", line 158, in createTests
    self.module)
  File "/usr/lib/python2.7/unittest/loader.py", line 130, in loadTestsFromNames
    suites = [self.loadTestsFromName(name, module) for name in names]
  File "/usr/lib/python2.7/unittest/loader.py", line 91, in loadTestsFromName
    module = __import__('.'.join(parts_copy))
ImportError: Import by filename is not supported.

(я использую Python 2.7.9.)


UPDATE

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

Любое из следующих действий будет приемлемым:

  • Как вызвать unittest из командной строки для запуска отдельных тестов или всех тестов в каталоге test/; для решения допустимо вносить небольшие изменения в код в сценариях тестирования (например, изменения в операторах импорта).

  • Если структура файла, показанная выше, по какой-либо причине невозможна, подробное объяснение будет приемлемым решением.

В качестве базового варианта начните со следующего минимального случая со следующей структурой файла:

myproj
├── a.py
├── b.py
└── test/
    ├── a.py
    └── b.py

... и следующее содержание

# a.py
def hello():
    print 'hello world'
# b.py
def bye():
    print 'good-bye world'
# test/a.py

import unittest
import a

class TestA(unittest.TestCase):
    def test_hello(self):
        self.assertEqual(a.hello(), None)
# test/b.py

import unittest
import b

class TestB(unittest.TestCase):
    def test_bye(self):
        self.assertEqual(b.bye(), None)

Покажите, как нужно unittest запустить тест test/a.py и как запустить "все тесты в test". (Последний должен продолжать работать, даже если новые сценарии тестирования добавляются в test или некоторые текущие сценарии тестирования удаляются.)


Минимальные тесты предложений, предложенных до сих пор, показывают, что они не работают. Например:

% python -m unittest discover -s test -p '*.py'
EE
======================================================================
ERROR: test_hello (a.TestA)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/SHIVAMJINDAL/myproj/test/a.py", line 6, in test_hello
    self.assertEqual(a.hello(), None)
AttributeError: 'module' object has no attribute 'hello'

======================================================================
ERROR: test_bye (b.TestB)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/SHIVAMJINDAL/myproj/test/b.py", line 6, in test_bye
    self.assertEqual(b.bye, None)
AttributeError: 'module' object has no attribute 'bye'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (errors=2)
% tree .
.
├── a.py
├── b.py
├── __init__.py
└── test/
    ├── a.py
    ├── b.py
    └── __init__.py

1 directory, 6 files

1 Это ограничение носит преднамеренный характер и является неотъемлемой частью представленной здесь проблемы. (Итак, «решение», которое влечет за собой ослабление этого ограничения, на самом деле не является решением.)

Ответы [ 8 ]

0 голосов
/ 15 мая 2018

Некоторое время назад у меня были похожие требования (python 2.6), и я в итоге реализовал небольшой исполняемый скрипт, который будет запускать ваши тесты (назовите его run_tests, код ниже), динамически загружая модули на основе путей к файлам.

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

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

Структура вашего проекта должна выглядеть следующим образом:

.
├── project
│   ├── a.py
│   └── __init__.py
└── test
    ├── a.py
    └── run_tests

Здесь project - это пакет, содержащий ваш исходный код a.py, помните, что для создания пакета в папке project требуется пустой файл __init__.py.

Теперь содержание run_tests:

#!/usr/bin/env python

import fnmatch
import imp
import os
import sys
import unittest

append_lib = lambda p: sys.path.append(p)
root_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
test_dir = os.path.join(root_dir, "test")

append_lib(root_dir) # packages to be tested are to be found there

def find_files():
  matches = []
  for root, dirnames, filenames in os.walk(test_dir):
    for filename in fnmatch.filter(filenames, "*.py"):
      matches.append(os.path.join(root, filename))
  return matches

def module_from_path(path):
  module = os.path.splitext(os.path.basename(path))[0]
  return imp.load_source(module, path)

def run_tests_in_paths(paths):
  suite = unittest.TestSuite()
  for p in paths:
    s = unittest.findTestCases(module_from_path(p))
    suite.addTests(s)
  if not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful():
    raise RuntimeError("some test cases failed")

if __name__ == "__main__":
  if len(sys.argv) > 1:
    paths = sys.argv[1:]
    print "> Running %s individual test file%s" % (len(paths), "s" if len(paths) > 1 else "")
    run_tests_in_paths(sys.argv[1:])
  else:
    print "> Running complete test suite"
    run_tests_in_paths(find_files())

Я протестировал эту настройку с этим содержимым для project/a.py:

class A(object):
  pass

А для test/a.py:

import unittest

from project import a

class TestA(unittest.TestCase):
  def test_hello(self):
    self.assertNotEqual(a.A(), None)

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

./test/run_tests test/a.py

Или как запустить все тестовые модули в папке test:

./test/run_tests

Примечание: не стесняйтесь улучшать / настраивать сценарии run_tests, чтобы они лучше соответствовали вашим потребностям.

Надеюсь, это поможет!

0 голосов
/ 13 мая 2018

Возможно ли реализовать проект Python с файловой структурой как следующее?

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

Например, выражение import a в вашем тестовом примере 'test \ a.py' разрешает использование модуля test\a вместо ссылки на тестируемый модуль (a.py)

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

+---python2unittest
¦       a.py
¦       b.py
¦       __init__.py
+---test
¦   ¦   a.py
¦   ¦   b.py

Ваш тестовый пример test / a.py будет выглядеть так:

import unittest

from python2unittest import a


class TestA(unittest.TestCase):

    def test_hello(self):
        self.assertEqual(a.hello(), None)

Как запустить тесты из командной строки:

python -m unittest discover -s  -t test -p a.py  // Specific Test
python -m unittest discover -s  -t test -p *.py  // All tests

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

Более подробную информацию о тестовом обнаружении можно найти здесь :

0 голосов
/ 14 мая 2018

Я думаю, что структура, которую вы показываете в своем вопросе, похожа на то, что вы ранее использовали какую-то IDE, такую ​​как Pycharm.

В Pycharm у вас может быть такая структура проекта. Как вы можете запустить unittest в одном файле, просто щелкнув правой кнопкой мыши по нему и нажав Run 'unittest in a.py'. И вы можете запустить unittest во всем каталоге, щелкнув правой кнопкой мыши по каталогу и выбрав Run 'unittest in test'.

Кроме того, вам не нужно создавать __ini__.py в test, чтобы сделать его модулем и импортируемым. Также вы можете напрямую import your_lib в тестовом файле, не беспокоясь о проблеме с путем импорта.

Все это делается магией IDE, поэтому вы не можете напрямую достичь этого, используя python -m unittest .... Если вы можете, почему Pycharm использует помощника по имени _jb_unittest_runner.py?

Но вы можете создать простой сценарий bash для достижения аналогичного поведения.

Ключи:

  • Назначение корня проекта для такой переменной, как PROJECT_HOME.
  • Назначение тестового корня такой переменной, как TEST_HOME.
  • Добавьте PROJECT_HOME к PYTHONPATH, чтобы вы могли напрямую импортировать свой проект в тестовые файлы.
  • Измените свой рабочий каталог на ${TEST_HOME}
  • Используйте python -m unittest ${PROJECT_HOME}/test/${file_name} для запуска один файл.
  • Используйте python -m unittest discover -s ${PROJECT_HOME}/test -t ${PROJECT_HOME}/test для запуска всего каталога.
  • Используйте python -m unittest ${file_name}.${test_case} для запуска одного теста.
0 голосов
/ 13 мая 2018

Я вполне уверен, что ваша проблема в том, что вы запускаете Python 2.7, где выполнение простого import a является неоднозначным, означает то, что относится к текущему модулю или из базового пути. Добавьте строку from __future__ import absolute_import к началу ваших файлов (это должна быть первая строка без комментариев). Я бы порекомендовал вам сделать это для всех файлов, но этого должно быть достаточно, чтобы сделать это для файлов test / *. Py.

Как только вы закончите, вы сможете запустить python -m unittest discover -s test -p '*.py'

0 голосов
/ 09 мая 2018

Ответ Таруна уже довольно полный ... Короче:

  1. Вы должны сделать ваши модули импортируемыми (создав __init__.py файлы)
  2. Вы должны указать интерпретатору, где именно искать, чтобы предотвратить конфликт имен между вашими модулями.

Импорт не обязательно должен быть относительным. Предполагая, что ваш проект будет упакован, импорт может (возможно, должен) быть точно таким, как вы ожидаете от пользователей вашего пакета - например, from myproj import a.

В этот момент у меня также есть python -m unittest discover -t .. -s test -p '*.py' для работы. Но в этот момент мне надоели дополнительные обручи, которые базовый пакет unittest помещает на пользователя. Я также рекомендовал бы, чтобы после внесения двух изменений вы также установили nosetests (строго говоря, пакет python nose), поскольку это обычно облегчает вам поиск и запуск тестов.

Например:

$ tree
.
├── a.py
├── a.pyc
├── b.py
├── b.pyc
├── __init__.py
├── __init__.pyc
└── test
    ├── a.py
    ├── a.pyc
    ├── b.py
    ├── b.pyc
    ├── __init__.py
    └── __init__.pyc

1 directory, 12 files
$ cat test/a.py
# test/a.py

import unittest
from myproj import a

class TestA(unittest.TestCase):
    def test_hello(self):
        self.assertEqual(a.hello(), None)
$ nosetests test/*.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
0 голосов
/ 09 мая 2018

Я смог заставить ваш подход работать, но есть несколько изменений, которые необходимы и обязательны.

  1. Тест должен выполняться в контексте родительского каталога
  2. Основная и тестовая папки должны иметь __init__.py
  3. Импорт в тестах должен быть относительным, а не прямым импортом

Итак, ниже моя древовидная структура

root@5db7ad85dafd:/project# tree
.
 __init__.py
 a.py
 test
     __init__.py
     a.py

1 directory, 4 files

root@5db7ad85dafd:/project# python --version
Python 2.7.9

Проект / a.py

hello = 'tarun'

Проект / тест / a.py

import unittest
from .. import a

class TestStringMethods(unittest.TestCase):
   def test_abc(self):
       assert a.hello == "tarun"

Обратите внимание на from .. import a, который является импортом для этой работы

Далее мы запускаем тест, находящийся в корневой папке проекта, как показано ниже

root@5db7ad85dafd:/project# python -m unittest discover -t .. -s test -p "*.py"
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

-t здесь задает каталог импорта верхнего уровня, поэтому наш относительный импорт может работать -s сообщает, в каком каталоге находятся наши тесты -p сообщает, какой шаблон должен быть обнаружен в

Когда вы хотите запустить отдельный тест, вы будете делать что-то вроде ниже

python -m unittest discover -t .. -s test -p "a.py"

или

python -m unittest discover -t .. -s test -p "*.py" a

Картинка всегда стоит больше слов

Unit Tests working

Edit-1

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

Так будет repo/project/test/a.py, и тогда в ваших тестах вы будете использовать

from project import a

и затем запустите его, как показано ниже, из папки репо

root@5db7ad85dafd:/repo# python -m unittest discover -v -t project -s project.test -p "*.py"
test_abc (test.a.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

или как ниже из тестовой папки

root@5db7ad85dafd:/repo/project# python -m unittest discover -v -t .. -s test -p "*.py"
test_abc (project.test.a.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

В этом случае, перемещая папку вашего проекта на один уровень от корня, убедитесь, что имя проекта не зависит от папки, в которой клонируется ваш проект

0 голосов
/ 09 мая 2018

Вам лучше упаковать свое приложение способом setuptools, это упростит задачу с обычной компоновкой файлов пакета.

В любом случае вы должны попробовать режим автообнаружения:

  • положить файл test/__init__.py. Даже пустой.
  • положить класс, наследующий unittest.TestCase в test/anything.py с test_xxx методом (ами)
  • cd root/of/project
  • python -m unittest discover -s test -p *.py

См. https://docs.python.org/2.7/library/unittest.html#test-discovery об используемых параметрах.

0 голосов
/ 07 мая 2018

Кажется, у вас нет файла __init__.py под вашим пакетом Python myproj и test. это вы не можете импортировать модуль в тесте.

Пожалуйста, добавьте пустой __init__.py файл в тестовый и myproj каталог оба. Если это не решит проблему, укажите код любого тестового файла.

Вы использовали импорт b в своем файле test / b.py, но он должен быть из импорта myproj b. Потому что, если вы используете просто import b, он импортирует текущий файл (в котором вы написали тест, и у этого файла нет метода bye ()). Так что вы можете изменить имя файла ваших тестовых файлов или использовать следующий код.

# test/b.py

import unittest
from myproj import b

class TestB(unittest.TestCase):
    def test_bye(self):
        self.assertEqual(b.bye(), None)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...