Como simular uma propriedade somente leitura com simulação?

93

Como você simula uma propriedade somente leitura com simulação ?

Eu tentei:

setattr(obj.__class__, 'property_to_be_mocked', mock.Mock())

mas o problema é que ele se aplica a todas as instâncias da classe ... o que quebra meus testes.

Você tem alguma outra ideia? Não quero zombar do objeto completo, apenas desta propriedade específica.

charlax
fonte

Respostas:

168

Acho que a melhor maneira é simular a propriedade como PropertyMock, em vez de simular o __get__método diretamente.

É declarado na documentação , pesquise por unittest.mock.PropertyMock: Um mock destinado a ser usado como uma propriedade, ou outro descritor, em uma classe. PropertyMockfornece __get__e __set__métodos para que você possa especificar um valor de retorno quando for buscado.

Aqui está como:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

def test(unittest.TestCase):
    with mock.patch('MyClass.last_transaction', new_callable=PropertyMock) as mock_last_transaction:
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()
Jamescastlefield
fonte
Tive que zombar de um método de classe decorado como @property. Essa resposta funcionou para mim quando a outra resposta (e outras respostas em muitas outras perguntas) não.
AlanSE
3
é assim que deve ser feito. Gostaria que houvesse uma maneira de mover a resposta "aceita"
vitiral
4
Acho que incluir o valor de retorno na chamada do gerenciador de contexto é um pouco mais limpo: `` `com mock.patch ('MyClass.last_transaction', new_callable = PropertyMock, return_value = Transaction ()): ...` ``
wodow
Na verdade, acabei de mover a resposta aceita para esta.
charlax de
1
usar mock.patch.object também é bom, pois você não precisa escrever o nome da classe como uma string (não é realmente um problema no exemplo) e é mais fácil de detectar / corrigir se você decidir renomear um pacote e não o fez atualizou um teste
Kevin
41

Na verdade, a resposta estava (como sempre) na documentação , é que eu estava aplicando o patch à instância em vez da classe quando segui o exemplo.

Veja como fazer:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

No conjunto de testes:

def test():
    # Make sure you patch on MyClass, not on a MyClass instance, otherwise
    # you'll get an AttributeError, because mock is using settattr and
    # last_transaction is a readonly property so there's no setter.
    with mock.patch(MyClass, 'last_transaction') as mock_last_transaction:
        mock_last_transaction.__get__ = mock.Mock(return_value=Transaction())
        myclass = MyClass()
        print myclass.last_transaction
charlax
fonte
14
as pessoas deveriam usar o outro exemplo. mock.PropertyMocké a maneira de fazer isso!
Vitiral
4
Isso mesmo, no momento da escrita PropertyMocknão existia.
charlax
6

Se o objeto cuja propriedade você deseja substituir for um objeto simulado, você não precisa usar patch.

Em vez disso, pode criar um PropertyMocke, em seguida, substituir a propriedade no tipo de simulação. Por exemplo, para substituir a mock_rows.pagespropriedade para retornar (mock_page, mock_page,):

mock_page = mock.create_autospec(reader.ReadRowsPage)
# TODO: set up mock_page.
mock_pages = mock.PropertyMock(return_value=(mock_page, mock_page,))
type(mock_rows).pages = mock_pages
Tim Swast
fonte
1
Bam, exatamente o que eu queria (objeto autospec'd com uma propriedade). E de um colega não menos 🙋‍♂️
Mark McDonald
6

Provavelmente uma questão de estilo, mas no caso de você preferir decoradores nos testes, a resposta de @ jamescastlefield pode ser alterada para algo assim:

class MyClass:
    @property
    def last_transaction(self):
        # an expensive and complicated DB query here
        pass

class Test(unittest.TestCase):
    @mock.patch('MyClass.last_transaction', new_callable=PropertyMock)
    def test(self, mock_last_transaction):
        mock_last_transaction.return_value = Transaction()
        myclass = MyClass()
        print myclass.last_transaction
        mock_last_transaction.assert_called_once_with()
Eyal Levin
fonte
5

Caso esteja usando pytestjunto com pytest-mock, você pode simplificar seu código e também evitar o uso do gerenciador de contexto, ou seja, a withinstrução da seguinte forma:

def test_name(mocker): # mocker is a fixture included in pytest-mock
    mocked_property = mocker.patch(
        'MyClass.property_to_be_mocked',
        new_callable=mocker.PropertyMock,
        return_value='any desired value'
    )
    o = MyClass()

    print(o.property_to_be_mocked) # this will print: any desired value

    mocked_property.assert_called_once_with()
lmiguelvargasf
fonte
2

Se você precisa do seu mock @propertypara confiar no original __get__, pode criar seuMockProperty

class PropertyMock(mock.Mock):

    def __get__(self, obj, obj_type=None):
        return self(obj, obj_type)

Uso:

class A:

  @property
  def f(self):
    return 123


original_get = A.f.__get__

def new_get(self, obj_type=None):
  return f'mocked result: {original_get(self, obj_type)}'


with mock.patch('__main__.A.f', new_callable=PropertyMock) as mock_foo:
  mock_foo.side_effect = new_get
  print(A().f)  # mocked result: 123
  print(mock_foo.call_count)  # 1
Conchylicultor
fonte
0

Se você não quiser testar se a propriedade simulada foi acessada ou não, você pode simplesmente corrigi-la com o esperado return_value.

with mock.patch(MyClass, 'last_transaction', Transaction()):
    ...
Simon Charette
fonte