2016-03-17 4 views
1

Я хочу протестировать развивающееся приложение базы данных SQLite, которое параллельно используется «продуктивно». Фактически я изучаю кучу больших текстовых файлов, импортируя их в базу данных и возиться с ней. Я привык разрабатывать тесты, и я не хочу отказываться от этого расследования. Но запущенные тесты против «производственной» базы данных кажутся несколько странными. Поэтому моя задача - запустить тесты против тестовой базы данных (реальной базы данных SQLite, а не макета), содержащей контролируемый, но значительный объем реальных данных, показывающих все виды изменчивости, которые я встречал во время расследования.«Стыковка где это определено» в python mock?

Чтобы поддержать этот подход, у меня есть центральный модуль myconst.py, содержащий функцию, возвращающую имя базы данных, которая используется как так:

import myconst 
conn = sqlite3.connect(myconst.get_db_path()) 

Сейчас в unittestTestCase с, я думал о насмешливый, как так :

@patch("myconst.get_db_name", return_value="../test.sqlite") 
def test_this_and_that(self, mo): 
    ... 

где тестовые вызовы функций, которые, во вложенных функций, доступ к базе данных с помощью myconst.get_db_path().

Сначала я попытался немного высмеивать for myself, но он имеет тенденцию быть неуклюжим и подверженным ошибкам, поэтому я решил погрузиться в модуль python mock, как показано выше.

К сожалению, я нашел warningsallover, что я должен «издеваться, где он используется, и не там, где он определен» так:

@patch("mymodule.myconst.get_db_name", return_value="../test.sqlite") 
def test_this_and_that(self, mo): 
    self.assertEqual(mymodule.func_to_be_tested(), 1) 

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

Итак, я попытался создать минимальный пример, чтобы понять поведение mock и где он не позволяет мне высмеивать «в источнике». Поскольку здесь много неудобств, я предоставил исходный код also on github для удобства каждого. Смотрите это:

myconst.py 
---------- 
# global definition of the database name 
def get_db_name(): 
    return "../production.sqlite" 
# this will replace get_db_name() 
TEST_VALUE = "../test.sqlite" 
def fun(): 
    return TEST_VALUE 

inner.py 
-------- 
import myconst 
def functio(): 
    return myconst.get_db_name() 
print "inner:", functio() 

test_inner.py 
------------- 
from mock import patch 
import unittest 
import myconst, inner 
class Tests(unittest.TestCase): 
    @patch("inner.myconst.get_db_name", side_effect=myconst.fun) 
    def test_inner(self, mo): 
     """mocking where used""" 
     self.assertEqual(inner.functio(), myconst.TEST_VALUE) 
     self.assertTrue(mo.called) 

outer.py 
-------- 
import inner 
def functio(): 
    return inner.functio() 
print "outer:", functio() 

test_outer.py 
------------- 
from mock import patch 
import unittest 
import myconst, outer 
class Tests(unittest.TestCase): 
    @patch("myconst.get_db_name", side_effect=myconst.fun) 
    def test_outer(self, mo): 
     """mocking where it comes from""" 
     self.assertEqual(outer.functio(), myconst.TEST_VALUE) 
     self.assertTrue(mo.called) 

unittests.py 
------------ 
"""Deeply mocking a database name...""" 
import unittest 
print(__doc__) 
suite = unittest.TestLoader().discover('.', pattern='test_*.py') 
unittest.TextTestRunner(verbosity=2).run(suite) 

test_inner.py работает как источники, связанные выше, скажем, и поэтому я ожидал, что это пройдет. test_outer.py должен потерпеть неудачу, если я правильно понимаю оговорки. Но все тесты проходят без жалобы! Таким образом, мой макет рисуется все время, даже когда издеваемую функцию вызывается из-под стопки, как в test_outer.py. Из этого примера я бы пришел к выводу, что мой подход безопасен, но, с другой стороны, предупреждения в разных источниках согласуются, и я не хочу безрассудно рисковать своей «производственной» базой данных, используя концепции, которые я не grok.

Итак, мой вопрос: неправильно ли я понимаю предупреждения или эти предупреждения слишком осторожны?

+0

Я скептически отношусь к вам из-за насмешек. Mocking полезно, когда вам нужно проверить, что объект * получил сообщение * (обычно это какая-то команда изменения состояния). Вы извлекаете данные конфигурации в середине функции. Это значительно улучшает тест на полную функцию, начинайте заканчивать. Для инструмента, который переносит данные из одного механизма хранения в другой, единственными вещами, которые я бы тестировал, являются фактические преобразования данных, которые вы выполняете в коде (т. Е. Не в SQL). И я бы удостоверился, что логика является чисто основанной на вводе/выводе, устраняющей необходимость в издевательствах. – jpmc26

+0

На самом деле, я хотел это сделать. Но преобразования создают «боковое содержимое» для базы данных (например, недавно найденные основные данные) и немедленно обновляют ее до базы данных. Это кажется странным с первого взгляда, но это делает код понятным, поэтому я не хочу реорганизовывать это. Однако во время других рефакторингов и улучшений я хочу иметь регрессию. Поэтому я произвел идею насмешки над конфигурацией. – flaschbier

+0

Что-то, что я нашел, поскольку я разработал больше, так это то, что очень часто очень полезно отделять «что» от «когда». Это означало бы, что вы можете иметь функции/классы, которые делают ваше преобразование, и возвращать данные назад, а затем другие функции для выполнения обновлений. Если вы не хотите менять организацию своего кода, я бы рекомендовал пройти полный интеграционный/функциональный/приемочный тест. Вместо того, чтобы издеваться над конфигурацией, переключитесь на фактический файл конфигурации. Используйте конфигурационный файл, содержащий параметры тестового env, когда вы тестируете. – jpmc26

ответ

1

Наконец-то я разобрался.Может быть, это поможет будущим посетителям, так что я буду делиться своими выводами:

При изменении кода like so:

inner.py 
-------- 
from myconst import get_db_name 
def functio(): 
    return get_db_name() 

test_inner.py 
------------- 
@patch("inner.get_db_name", side_effect=myconst.fun) 
def test_inner(self, mo): 
    self.assertEqual(inner.functio(), myconst.TEST_VALUE) 

test_inner преуспеет, но test_outer сломается с

AssertionError: '../production.sqlite' != '../test.sqlite' 

Это происходит потому, что mock.patch не заменит ссылочный объект, который является функцией get_db_name в модуле myconst в обоих случаях. mock вместо этого заменит обычаи "myconst.get_db_name" объектом Mock, переданным в качестве второго параметра для испытания.

test_outer.py 
------------- 
@patch("myconst.get_db_name", side_effect=myconst.fun) 
def test_outer(self, mo): 
    self.assertEqual(outer.functio(), myconst.TEST_VALUE) 

Поскольку я издеваться только "myconst.getdb_name" здесь и inner.py доступ get_db_name через "inner.get_db_name", тест проваливается.

Используя собственное имя, однако, это может быть исправлено:

@patch("outer.inner.get_db_name", return_value=myconst.TEST_VALUE) 
def test_outer(self, mo): 
    self.assertEqual(outer.functio(), myconst.TEST_VALUE) 

Поэтому вывод, что мой подход будет безопасным когда я убедитесь, что все модули, обращающиеся к базе данных включают myconst и использования myconst.get_db_name. Альтернативно все модули могут from myconst import get_db_name и использовать get_db_name. Но я должен принять это решение по всему миру.

Потому что я контролирую весь код доступа get_db_name Я в безопасности. Можно утверждать, что это хороший стиль или нет (предположительно последний), но технически это безопасно. Если бы я высмеивал библиотечную функцию, я вряд ли мог бы контролировать доступ к этой функции, и поэтому насмешка «там, где она определена» становится рискованной. Вот почему цитируемые источники являются предупреждением.

Смежные вопросы