Zombando da função python com base em argumentos de entrada

150

Estamos usando o Mock para python há um tempo.

Agora, temos uma situação em que queremos zombar de uma função

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result

Normalmente, a maneira de zombar disso seria (assumindo que parte de um objeto)

self.foo = MagicMock(return_value="mocked!")

Mesmo se eu chamar foo () algumas vezes, eu posso usar

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])

Agora, estou enfrentando uma situação na qual desejo retornar um valor fixo quando o parâmetro de entrada tiver um valor específico. Então, se vamos dizer "my_param" é igual a "alguma coisa", quero retornar "my_cool_mock"

Parece estar disponível no mockito para python

when(dummy).foo("something").thenReturn("my_cool_mock")

Estive pesquisando sobre como conseguir o mesmo com Mock sem sucesso?

Alguma ideia?

Juan Antonio Moriano, Gomez
fonte
2
Pode ser esta resposta vai ajudar - stackoverflow.com/a/7665754/234606
naiquevin
@naiquevin Isso resolve perfeitamente o problema, companheiro!
Juan Antonio Gomez Moriano
Eu não tinha ideia de que você poderia usar o Mocktio com Python, +1 para isso!
Ben
Se o seu projeto usa pytest, para esse fim, você pode aproveitar monkeypatch. O Monkeypatch é mais para "substituir essa função para fins de teste", enquanto o Mock é o que você usa quando também deseja verificar mock_callsou fazer afirmações sobre como foi chamado e assim por diante. Há um lugar para ambos, e geralmente os uso em momentos diferentes em um determinado arquivo de teste.
Driftcatcher 8/10/19

Respostas:

187

Se side_effectfor uma função, o que quer que essa função retorne é o que chama o retorno simulado. A side_effectfunção é chamada com os mesmos argumentos que a simulação. Isso permite que você altere o valor de retorno da chamada dinamicamente, com base na entrada:

>>> def side_effect(value):
...     return value + 1
...
>>> m = MagicMock(side_effect=side_effect)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[call(1), call(2)]

http://www.voidspace.org.uk/python/mock/mock.html#calling

Âmbar
fonte
25
Apenas para facilitar a resposta, você poderia renomear a função side_effect para outra coisa? (eu sei, eu sei, é muito simples, mas melhora a legibilidade do fato de que o nome da função e param name são diferentes :)
Juan Antonio Gomez Moriano
6
@JuanAntonioGomezMoriano Eu poderia, mas neste caso estou apenas citando diretamente a documentação, por isso estou com um pouco de ódio de editar a citação se ela não estiver especificamente quebrada.
23413 Amber
e apenas para ser pedante todos estes anos mais tarde, mas side effecté o termo exato: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh
7
@Ish eles não estão reclamando do nome de CallableMixin.side_effect, mas que a função separada definida no exemplo tem o mesmo nome.
1811 OrangeDog
48

Conforme indicado no objeto Python Mock com o método chamado várias vezes

Uma solução é escrever meu próprio side_effect

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

Isso faz o truque

Juan Antonio Moriano, Gomez
fonte
2
Isso tornou mais claro para mim do que a resposta selecionada, então obrigado por responder a sua própria pergunta :)
Luca Bezerra
15

O efeito colateral assume uma função (que também pode ser uma função lambda ); portanto, em casos simples, você pode usar:

m = MagicMock(side_effect=(lambda x: x+1))
Shubham Chaudhary
fonte
4

Acabei aqui procurando " como zombar de uma função baseada em argumentos de entrada " e finalmente resolvi isso criando uma função aux simples:

def mock_responses(responses, default_response=None):
  return lambda input: responses[input] if input in responses else default_response

Agora:

my_mock.foo.side_effect = mock_responses(
  {
    'x': 42, 
    'y': [1,2,3]
  })
my_mock.goo.side_effect = mock_responses(
  {
    'hello': 'world'
  }, 
  default_response='hi')
...

my_mock.foo('x') # => 42
my_mock.foo('y') # => [1,2,3]
my_mock.foo('unknown') # => None

my_mock.goo('hello') # => 'world'
my_mock.goo('ey') # => 'hi'

Espero que isso ajude alguém!

Manu
fonte
2

Você também pode usar partialfrom functoolsse quiser usar uma função que aceita parâmetros, mas a função que você está zombando não. Por exemplo:

def mock_year(year):
    return datetime.datetime(year, 11, 28, tzinfo=timezone.utc)
@patch('django.utils.timezone.now', side_effect=partial(mock_year, year=2020))

Isso retornará uma chamada que não aceita parâmetros (como o fuso horário do Django.now ()), mas minha função mock_year aceita.

Hernan
fonte
Obrigado por esta solução elegante. Gostaria de acrescentar que, se sua função original tiver parâmetros adicionais, eles deverão ser chamados com palavras-chave no código de produção ou essa abordagem não funcionará. Você obter o erro: got multiple values for argument.
Erik Kalkoken
1

Apenas para mostrar outra maneira de fazer isso:

def mock_isdir(path):
    return path in ['/var/log', '/var/log/apache2', '/var/log/tomcat']

with mock.patch('os.path.isdir') as os_path_isdir:
    os_path_isdir.side_effect = mock_isdir
caleb
fonte
1

Você também pode usar @mock.patch.object:

Digamos que um módulo my_module.pyuse pandaspara ler um banco de dados e gostaríamos de testá-lo usando o pd.read_sql_tablemétodo de zombaria (que toma table_namecomo argumento).

O que você pode fazer é criar (dentro do seu teste) um db_mockmétodo que retorne objetos diferentes, dependendo do argumento fornecido:

def db_mock(**kwargs):
    if kwargs['table_name'] == 'table_1':
        # return some DataFrame
    elif kwargs['table_name'] == 'table_2':
        # return some other DataFrame

Na sua função de teste, você faz:

import my_module as my_module_imported

@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
    # You can now test any methods from `my_module`, e.g. `foo` and any call this 
    # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
    ret = my_module_imported.foo(table_name='table_1')
    # `ret` is some DataFrame returned by `db_mock`
Tomasz Bartkowiak
fonte
1

Se você "deseja retornar um valor fixo quando o parâmetro de entrada tiver um valor específico" , talvez você nem precise de um mock e possa usar um dictjunto com seu getmétodo:

foo = {'input1': 'value1', 'input2': 'value2'}.get

foo('input1')  # value1
foo('input2')  # value2

Isso funciona bem quando a saída do seu falso é um mapeamento de entrada. Quando é uma função de entrada, sugiro usar de side_effectacordo com a resposta de Amber .

Você também pode usar uma combinação de ambos se quiser preservar Mockos recursos ( assert_called_once, call_countetc):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Arseniy Panfilov
fonte
Isso é muito inteligente.
emyller 30/06
-6

Eu sei que é uma pergunta bastante antiga, pode ajudar como uma melhoria usando python lamdba

self.some_service.foo.side_effect = lambda *args:"Called with 42" \
            if args[0] == 42 \
            else "Called with 42" if args[0] == 43 \
            else "Called with 43" if args[0] == 43 \
            else "Called with 45" if args[0] == 45 \
            else "Called with 49" if args[0] == 49 else None
Novato
fonte