Afirme que um método foi chamado em um teste de unidade Python

95

Suponha que eu tenha o seguinte código em um teste de unidade Python:

aw = aps.Request("nv1")
aw2 = aps.Request("nv2", aw)

Existe uma maneira fácil de afirmar que um método específico (no meu caso aw.Clear()) foi chamado durante a segunda linha do teste? por exemplo, há algo assim:

#pseudocode:
assertMethodIsCalled(aw.Clear, lambda: aps.Request("nv2", aw))
Mark Heath
fonte

Respostas:

153

Eu uso o Mock (que agora é unittest.mock em py3.3 +) para isso:

from mock import patch
from PyQt4 import Qt


@patch.object(Qt.QMessageBox, 'aboutQt')
def testShowAboutQt(self, mock):
    self.win.actionAboutQt.trigger()
    self.assertTrue(mock.called)

Para o seu caso, poderia ser assim:

import mock
from mock import patch


def testClearWasCalled(self):
   aw = aps.Request("nv1")
   with patch.object(aw, 'Clear') as mock:
       aw2 = aps.Request("nv2", aw)

   mock.assert_called_with(42) # or mock.assert_called_once_with(42)

O Mock suporta alguns recursos úteis, incluindo maneiras de corrigir um objeto ou módulo, bem como verificar se a coisa certa foi chamada, etc, etc.

Caveat emptor! (Cuidado, comprador!)

Se você digitar incorretamente assert_called_with(para assert_called_onceou assert_called_wiht) seu teste ainda poderá ser executado, pois o Mock pensará que esta é uma função simulada e felizmente continuará, a menos que você use autospec=true. Para obter mais informações, leia assert_called_once: Ameaça ou Ameaça .

Macke
fonte
5
+1 para iluminar discretamente meu mundo com o maravilhoso módulo Mock.
Ron Cohen
@RonCohen: Sim, é incrível e está cada vez melhor também. :)
Macke
1
Embora usar o mock seja definitivamente o caminho a seguir, eu desaconselho o uso de assert_called_once, com simplesmente não existe :)
FelixCQ
ele foi removido em versões posteriores. Meus testes ainda estão usando. :)
Macke de
1
Vale a pena repetir como é útil usar autospec = True para qualquer objeto simulado, porque ele pode realmente morder você se você digitar incorretamente o método assert.
rgilligan
31

Sim, se você estiver usando o Python 3.3+. Você pode usar o unittest.mockmétodo integrado para declarar chamado. Para Python 2.6+, use o roll backport Mock, que é a mesma coisa.

Aqui está um exemplo rápido no seu caso:

from unittest.mock import MagicMock
aw = aps.Request("nv1")
aw.Clear = MagicMock()
aw2 = aps.Request("nv2", aw)
assert aw.Clear.called
Devy
fonte
14

Não estou ciente de nada embutido. É muito simples de implementar:

class assertMethodIsCalled(object):
    def __init__(self, obj, method):
        self.obj = obj
        self.method = method

    def called(self, *args, **kwargs):
        self.method_called = True
        self.orig_method(*args, **kwargs)

    def __enter__(self):
        self.orig_method = getattr(self.obj, self.method)
        setattr(self.obj, self.method, self.called)
        self.method_called = False

    def __exit__(self, exc_type, exc_value, traceback):
        assert getattr(self.obj, self.method) == self.called,
            "method %s was modified during assertMethodIsCalled" % self.method

        setattr(self.obj, self.method, self.orig_method)

        # If an exception was thrown within the block, we've already failed.
        if traceback is None:
            assert self.method_called,
                "method %s of %s was not called" % (self.method, self.obj)

class test(object):
    def a(self):
        print "test"
    def b(self):
        self.a()

obj = test()
with assertMethodIsCalled(obj, "a"):
    obj.b()

Isso requer que o próprio objeto não modifique self.b, o que quase sempre é verdadeiro.

Glenn Maynard
fonte
Eu disse que meu Python estava enferrujado, embora tenha testado minha solução para ter certeza de que funciona :-) Eu internalizei o Python antes da versão 2.5, na verdade nunca usei o 2.5 para nenhum Python significativo, pois tivemos que congelar em 2.3 para compatibilidade de lib. Ao revisar sua solução, encontrei effbot.org/zone/python-with-statement.htm como uma boa descrição clara. Eu humildemente sugeriria que minha abordagem parece menor e pode ser mais fácil de aplicar se você quiser mais de um ponto de registro, em vez de "com" s aninhados. Eu realmente gostaria que você explicasse se há algum benefício particular seu.
Andy Dent,
@Andy: Sua resposta é menor porque é parcial: na verdade não testa os resultados, não restaura a função original após o teste para que você possa continuar usando o objeto e você tem que escrever repetidamente o código para fazer tudo isso novamente cada vez que você escreve um teste. O número de linhas do código de suporte não é importante; esta classe vai em seu próprio módulo de teste, não embutido em uma docstring - isso leva uma ou duas linhas de código no teste real.
Glenn Maynard,
6

Sim, posso dar o esboço, mas meu Python está um pouco enferrujado e estou muito ocupado para explicar em detalhes.

Basicamente, você precisa colocar um proxy no método que irá chamar o original, por exemplo:

 class fred(object):
   def blog(self):
     print "We Blog"


 class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth

   def __call__(self, code=None):
     self.meth()
     # would also log the fact that it invoked the method

 #example
 f = fred()
 f.blog = methCallLogger(f.blog)

Esta resposta StackOverflow sobre chamável pode ajudá-lo a entender o acima.

Em mais detalhes:

Embora a resposta tenha sido aceita, devido à interessante discussão com Glenn e por ter alguns minutos livres, eu queria ampliar minha resposta:

# helper class defined elsewhere
class methCallLogger(object):
   def __init__(self, meth):
     self.meth = meth
     self.was_called = False

   def __call__(self, code=None):
     self.meth()
     self.was_called = True

#example
class fred(object):
   def blog(self):
     print "We Blog"

f = fred()
g = fred()
f.blog = methCallLogger(f.blog)
g.blog = methCallLogger(g.blog)
f.blog()
assert(f.blog.was_called)
assert(not g.blog.was_called)
Andy Dent
fonte
bom. Eu adicionei uma contagem de chamadas para methCallLogger para que eu possa afirmar isso.
Mark Heath
Isso em relação à solução completa e independente que forneci? A sério?
Glenn Maynard
@Glenn Sou muito novo em Python - talvez o seu seja melhor - só não entendo tudo ainda. Vou passar um pouco de tempo depois experimentando.
Mark Heath
Esta é de longe a resposta mais simples e fácil de entender. Trabalho muito bom!
Matt Messersmith
4

Você pode simular aw.Clearmanualmente ou usando uma estrutura de teste como o pymox . Manualmente, você faria isso usando algo assim:

class MyTest(TestCase):
  def testClear():
    old_clear = aw.Clear
    clear_calls = 0
    aw.Clear = lambda: clear_calls += 1
    aps.Request('nv2', aw)
    assert clear_calls == 1
    aw.Clear = old_clear

Usando o pymox, você faria assim:

class MyTest(mox.MoxTestBase):
  def testClear():
    aw = self.m.CreateMock(aps.Request)
    aw.Clear()
    self.mox.ReplayAll()
    aps.Request('nv2', aw)
Max Shawabkeh
fonte
Eu gosto dessa abordagem também, embora ainda queira que old_clear seja chamado. Isso torna óbvio o que está acontecendo.
Mark Heath