O PyUnit é uma biblioteca para realização de testes unitários em Python baseada na arquitetura xUnit. É a forma mais difundida para realizar a prática de testes unitários pela comunidade Python.

Visão Geral

O PyUnit foi criado com o intuito de trazer ao Python todos os recursos que já haviam no framework de teste unitário JUnit da linguagem Java, a ferramenta evoluiu de forma a explorar plenamente os recursos da linguagem e se adequar ao jeito pythonico de escrever código.

Por exemplo, quando precisamos testar se um método de consulta está buscando o registro certo, o mais comum é chamar esse método dentro da cláusula if __name__ == "__main__":, imprimir o resultado na tela e executar o módulo onde o método é declarado.


        if __name__ == "__main__":
        cliente_dao = ClienteDAO()
        cliente = cliente_dao.find_by_id(10)
        print(cliente.name())
        # o resultado esperado é "Edson Arantes do Nascimento"
        

Em um projeto maior esse tipo de teste tende a se tornar inviável e inseguro a medida que a complexidade do módulo e da unidade aumentam, é natural sentir falta de uma metodologia de testes, que permita medir a cobertura de teste do código, tornando fácil criar os cenários nos quais as falhas serão percebidas. Os testes unitários suprem essas e outras carências do processo de teste de código.

def test_find_by_id(self):
        cliente_dao = ClienteDAO()
        cliente = cliente_dao.find_by_id(10)
        self.assertEqual(cliente_dao, 'Edson Arantes do Nascimento')
        

Testes Unitários são escritos quase sempre com o auxílio de uma ferramenta, no caso do Python o programador conta com uma ferramenta tão bem adaptada à linguagem e tão bem aceita pela comunidade que está implementada como biblioteca nativa desde a versão 2.1 do Python em diante.

Instalação

O PyUnit é presente por padrão no Python desde a versão 2.1 como biblioteca de nome unittest e por isso não precisa ser instalado. O PyUnit pode ser implementado importando o módulo unittest inteiro ou apenas os recursos que vão ser usados:


            import unittest
        

Ou importando especificamente algum recurso:

from unittest import <recurso a ser usado>
        

Configuração

Por ser utilizado nativamente como uma biblioteca, o PyUnit não precisa de nenhuma configuração prévia para executar os testes.

Escrevendo Testes

Para criar testes com PyUnit é necessário criar uma Classe de Testes que deve extender a classe TestCase presente no módulo unittest.

Visão geral

O PyUnit localiza os métodos de teste pelo prefixo test em seu nome. Por fim é necessário chamar a função main do módulo unittest dentro da expressão if __name__ == "__main__": do módulo para executar os testes criados, como ilustrado abaixo:


            import unittest

        class TestClass(unittest.TestCase):

        def test_meu_metodo(self):
        self.assertEqual(valor_esperado , valor_real, "mensagem caso o teste falhe")

        if __name__ == "__main__":
        unittest.main()
        
  • Linha 6: é chamado o método assertEqual é um método assert da classe TestCase. Os métodos assert (do inglês afirmar) são responsáveis por verificar se o resultado gerado durante o teste é igual ao esperado, caso contrário o teste resultará em uma falha e exibirá a mensagem, caso definida.
    
                        self.assertEqual(valor_esperado , valor_real, "mensagem caso o teste
                    falhe")
                    

Por exemplo, considere que queremos testar isoladamente o seguinte método frete_gratis da classe Compra:


        class Compra:
        def frete_gratis(self, valor):
        return valor >= 150
        

Criamos então a classe CompraTest dentro da pasta tests, como o método test_frete_gratis, de acordo com o código abaixo.


            import unittest
        from compra import Compra

        class CompraTeste(unittest.TestCase):
        def test_frete_gratis(self):
        nova_compra = Compra()
        self.assertTrue(nova_compra.frete_gratis(200))

        if __name__ == "__main__":
        unittest.main()
        

O método assertTrue define que o retorno de nova_compra.frete_gratis(200) deve ser True caso contrário o teste falha.

Na prática

Exemplo 1

Em certas situações é necessário saber se uma unidade irá gerar um erro, ou Exception. Para esse tipo de situação, o PyUnit possui um método específico chamado assertRaises() que determina se o código sendo testado deve disparar uma exceção e qual tipo de exceção deve ser disparada. O método assertRaises possui a seguinte sintaxe:


            assertRaise(tipo_erro, codigo_testado [, parâmetros, caso, existam)]

Na prática sua utilização é muito simples: ele é declarado como um assert comum porém em seu primeiro parâmetro é informado o tipo do erro, em seguida o nome da função que será testada (sem os () de chamada) e por fim serão declarados os parâmetros, caso existam, que devem ser atribuídos à função sendo testada pelo assertRaises.


            def test_frete_gratis_exception(self):
        nova_compra = Compra()
        self.assertRaises(TypeError, nova_compra.frete_gratis, "duzentos")
        

Se a unidade testada com assertRaises não disparar uma exceção o teste resultará em uma falha.

Exemplo 2

É uma declaração nativa do Python que verifica se a expressão declarada a seguir tem um valor passável como true em um if. Caso não seja irá disparrar um AssertionError e exibir uma mensagem de erro, caso definida.


            assert expressao [, "mensagem"]
            

Quando usada em um método de testes a declaração assert se comporta de maneira similar com os métodos assert, como demonstrado abaixo:

import unittest
        from compra import Compra

        class CompraTeste(unittest.TestCase):
        def test_frete_gratis(self):
        nova_compra = Compra()
        assert nova_compra.frete_gratis(100), "erro em Compra.frete_gratis"

        if __name__ == "__main__":
        unittest.main()
        

Executando testes

Para executar os testes basta rodar o arquivo no terminal com o comando python.

python test_module.py
        

Então o terminal de linha de comando dará uma resposta semelhante a seguinte se nenhum erro for detectado nos testes:

> python classe_de_teste.py
        ..
        ----------------------------------------------------------------------
        Ran 2 tests in 0.001s

        OK
        

No exemplo acima o PyUnit está indicando que dois testes foram executados e nenhum erro foi encontrado.

Ambiente

Em muitas situações é necessário reutilizar instâncias em mais de um teste dentro de uma classe de teste. O ambiente de testes consiste dos objetos que serão utilizados em todos os testes da classe de testes.

Visão geral

O PyUnit possui as seguintes ferramentas para a montagem e desmontagem de ambientes de teste:

  • setUp: é o método da superclasse TestCase que é executado antes de cada teste na mesma classe, ideal para instanciar objetos com valores predefinidos.
  • tearDown: é o método da superclasse TestCase que é executado após cada teste na mesma classe, ideal para fechar conexões ao banco de dados, arquivos, etc.

Embora esse seja um recurso útil, devemos evitar utilizar tais métodos quando forem desnecessários, uma vez que eles podem tornar a leitura do código mais difícil, visto que para acompanhar o que o código faz o programador deverá dividir a sua atenção entre alguns métodos.

Na prática

Exemplo 1

Sabendo que grandes aplicações costumam executar seus testes de forma autônoma, em um processo que pode durar horas até que todo o sistema esteja coberto por eles. Nesse contexto, é importante que cada teste na pilha possa ser executado sem nenhuma conexão com o anterior como uma garantia de que a falha em um determinado teste não comprometerá a confiabilidade dos seguintes. De outra forma, pode ser necessário rastrear a origem problemas como testes que falham dependendo da ordem em que são executados, o que pode facilmente consumir algumas horas do programador.

O método setUp é usado principalmente para criar implementações que serão compartilhadas por todos os testes, como objetos com muitos atributos:

import unittest

        from funcionario import Funcionario
        from folha_pagamento import FolhaPagamento

        class FolhaPagamentoTest(unittest.TestCase):
        def setUp(self):
        self.funcionario = Funcionario(3000, "Cláudio André Mergen Taffarel", 1, 171.9)
        self.folha = FolhaPagamento()

        def test_calcula_salario_liquido_controle(self):
        self.assertEqual(2222.07, folha.calcula_salario_liquido(self.funcionario))

        def test calcula_salario_liquido_isento_INSS(self):
        self.funcionario.salario = 1693.72
        self.assertEqual(1366.6, folha.calcula_salario_liquido(self.funcionario))

        def test_calcula_salario_liquido_tres_dependentes(self):
        self.funcionario.dependentes = 3
        self.assertEqual(2250.51, folha.calcula_salario_liquido(self.funcionario))
        

Exemplo 2

O método tearDown tem como propósito permitir a recuperação do estado original antes após cada teste ter sido executado. Considere que tenhamos uma classe de log disponível dessa forma, que é obtida a partir de uma fábrica e que ao realizar suas ações mantém em memória dados como o nome do arquivo utilizado, um buffer do seu conteúdo, entre outras coisas.

import unittest

        from analizador import Analizador

        class AnalizadorTest(unittest.TestCase):
        def setUp(self):
        self.analizador = Logger.analizador

        def test_nome_arquivo_log_valido_letras_maiusculas(self):
        resultado = analizador.nome_arquio_log_valido("log_salvo.LOG")
        self.assertTrue(resultado)

        def test_nome_arquivo_log_valido_letras_minusculas(self):
        resultado = analizador.nome_arquio_log_valido("log_salvo.log")
        self.assertTrue(resultado)

        def tearDown(self):
        self.analizador = null
        

Dessa forma, se algum teste posterior a esse utilizar a mesma classe de log, os dados armazenados durante a execução deste teste não colocarão em dúvida o seu resultado.

Exemplo 3

Os métodos setUp e tearDown são comumente vistos em conjunto, como é o caso no código abaixo. Entretanto esse é um dos exemplos de caso de usa incorreto desse recurso e que configura uma má prática, visto que apenas pelo escopo no qual os objetos são utilizadas podemos definir o seu ciclo de vida.

import unittest

        from funcionario import Funcionario
        from helpers import Connection

        class FuncionarioTest(unittest.TestCase):
        def setUp(self):
        self.funcionario = Funcionario(3000, "José", 1, 171.9)
        self.conexao = Connection()
        self.conexao.open()

        def tearDown(self):
        self.conexao.close()
        

Uma outra versão desse código pode ser melhor construída se utilizarmos um classe de fábrica para construir a conexão, fazendo sua inicialização e destruição dentro de um mesmo método. Dessa forma, ficará mais claro para o programador qual o propósito deste método de teste. Vejamos esse segundo exemplo.

import unittest

        from funcionario import Funcionario
        from helpers import Connection

        class FuncionarioTest(unittest.TestCase):
        def setUp(self):
        self.funcionario = Funcionario(3000, "José", 1, 171.9)
        self.conexao = getConnection()
        self.conexao.open()
        ...

        def getConexao(self):
        return Connection()
        

Objetos Mock

Visão Geral

Considerando que desejamos validar um modelo a ser persistido, podemos usar um objeto mock para substituir a classe de persistência, que nesse caso é necessária para a completude da ação, mas ainda assim indiretamente importante. Dessa forma, podemos nos concentrar na validação e isentar o código de teste de erros de conexão, entre outros.

Um mock é um objeto falso construído a partir de uma classe. Geralmente criamos esse objeto para utilizá-lo em lugar de uma dependência do cenário a ser testado. Assim, tornamos o código de teste isento a erros indiretamente relacionados ao objetivo principal em um determinado contexto de teste.

Na prática

Exemplo 1

Usamos a anotação @patch para criar um objeto mock vinculado a classe que precisa ser simulada dentro do teste, por padrão os métodos desse mock não fazem nada. Lembre-se que um mock é um objeto falso, que pode ser criado a partir de uma classe, mas não é uma instância dessa classe. Quando preciamos que um método de um mock gerado desse jeito retorne algo criamos um novo mock para esse método e informamos qual valor esse mock deve retornar quando for chamado no construtor da classe Mock como valor de return_value.

Como exemplo, considere que queremos testar o seguinte método que recebe um valor e calcula sua conversão de uma outra moeda:

from api.helpers.currency import Currency

        class FolhaPagamento:

        def pagamento_moeda_estrangeira(self, tipo_moeda, valor, currency: Currency):
        if (tipo_moeda == Currency.QUOTACAO_DOLAR):
        return valor * currency.get_quotacao_dolar()
        elif(tipo_moeda == Currency.QUOTACAO_EURO):
        return valor * currency.get_quotacao_euro()
        elif(tipo_moeda == Currency.QUOTACAO_LIBRA):
        return valor * currency.get_quotacao_libra()
        else:
        raise ValueError("moeda não disponível")
        return 0
        

Para um primeiro teste vamos criar um mock da classe Currency e definir que seu método get_quotacao_dolar vai ter um retorno igual a 3,00. Com isso desejamos simular uma chamada a uma API que forneça o valor do dolar do dia.

class FolhaPagamentoTeste(unittest.TestCase):

        @patch('helpers.Currency')
        def test_pagamento_dolar(self, fake_currency):
        folha = FolhaPagamento()
        fake_currency.get_quotacao_dolar = Mock(return_value=3)
        resultado = folha.pagamento_moeda_estrangeira("dolar", 3000, fake_currency)
        assert resultado == 9000, "valor incorreto"

  • Linha 3: A anotação @patch define que o argumento do teste (exceto o self de um método) será um objeto mock do tipo Currency dentro do escopo desse teste.
    @patch('nome.da.Classe')
                    
  • Linha 6: é criado um mock para o método get_quotacao_dolar de fake_currency com um valor de retorno definido através do parametro return_value do construtor da classe Mock()

O decorator patch facilita criar classes ou objetos mock no módulo sob teste. O atributo em questão (exceto o self de um método) será substituído por um mock durante o teste e restaurado após o término do teste.

A classe Mock

Mock é um objeto flexível planejado para substituir o uso de esboços e dublês de teste em seu código. Um Mock pode ser chamado e criar atributos como novos mocks quando acessados. Acessar os mesmo atributos vão sempre retornar os mesmos mocks. Mocks registram como são usados, permitindo que assertions sejam feitos sobre a interação com seu código.

class unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, kwargs)

Argumentos

  • spec:

    Define quais atributos serão usados durante o teste. Aceita uma lista de string ou um objeto. Qualquer atributo do mock que for chamado deve ser declarado em spec ou um AtributeError será disparado, porém permite atribuição\criação de novos atributos. Caso um objeto seja passando em spec, o método mágico __class__ retornará a classe do spec, isso permite que mocks passem em testes de isinstance().

  • spec_set

    É uma versão mais rigorosa de spec. Impede qualquer referência a um atributo que não seja declarado em spec_set

  • side_effect

    Pode ser tanto uma função a ser executada quando o mock for chamado, um iterável ou uma exceção (classe ou instância) as ser disparada.

    Caso seja passada uma função ela será chamada com os mesmo argumentos que o mock e, a menos que a função tenha o retorno padrão do mock, o valor de retorno da função será o utilizado.

    Alternativamente, side_effect pode ser uma classe ou instância de exceção. Nese caso uma exception será disparada quando o mock for chamado.

    Caso side_effect seja um iterável então cada chamada resultará no próximo valor do iterável.

    side_effect pode ser setado como None para esvaziá-lo.

  • return_value

    Declara o valor de retorno quando o mock é executado. Por padrão é um novo mock.

  • unsafe

    Por padrão, se algum atributo do mock começar com assert ou assret um AttributeError será disparado. Passar unsafe=True faz permitir o uso de atributos com esses nomes.

  • wraps

    Adiciona um objeto para ser envolto pelo mock, isto é, se wraps não for None fazer uma chamado para o mock será o mesmo que chamar o objeto que foi definido. Acessar um atributo nesse mock retornará um mock que envolve o atributo correspondente do objeto que foi originalmente definido em wraps. (Acessar um atributo inexistente vai disparar um AtributeError)

  • name

    Define um nome para o mock que será usado como seu __repr__. O nome se propaga em mocks filhos.

Métodos de Mock

  • assert_called(*args, kwargs)

    Assegura que o mock foi chamado pelo menos uma vez. Por exemplo quando queremos garantir que um método "realizacompra" vai executar o método "recebepagamento" de um gateway de pagamento. Falha se o mock não for chamado.

def test_recebe_pagamento_pelo_menos_uma_vez(self):
        gateway = Mock()
        compra = Compra(gateway) 
        compra.realiza_compra()
        gateway.recebe_pagamento.assert_called()
    
  • assertcalledonce(*args, kwargs)

    Semelhante ao "assert_called" porém assegura que o mock foi chamado exatamente uma vez. Falha se o mock não for chamado ou for chamado mais de uma vez.

def test_recebe_pagamento_exatamente_uma_vez(self):
        gateway = Mock()
        compra = Compra(gateway) 
        compra.realiza_compra()
        gateway.recebe_pagamento.assert_called_once()
    
  • assertcalledwith(*args, kwargs)

    Assegura que o mock foi chamado com parâmetros definidos em sua chamada mais recente

def test_recebe_pagamento_exatamente_uma_vez(self):
        gateway = Mock()
        compra = Compra(gateway) 
        compra.realiza_compra(3000)
        gateway.recebe_pagamento.assert_called_with(3000)
    
  • assertcalledonce_with(*args, kwargs)

    Semelhante a "assertcalledwith", assegura que o mock foi chamado exatamente uma vez com parâmetros definidos em sua chamada mais recente.

  • assertanycall(*args, kwargs)

    Assegura que o mock foi chamado com os parâmetros definidos pelo menos uma vez, independente de quaisquer outra vez que tenha sido chamado, ou outros parâmetros que tenham sido passados a mais.

def test_escreve_arquivo(self):
        text = Mock()
        writer = Writer(text)
        writer.write("primeira linha")
        writer.write("segunda linha")
        writer.write("terceira linha")
        text.save.assert_any_call("segunda linha")
    
  • asserthascalls(calls, any_order=False) Assegura que o mock foi chamado com ao menos algum objeto call específico.

    Enquanto any_order for False (por padrão) as calls precisarão ser sequenciais.

    def test_recebe_pagamento_executou_duas_calls(self):
        gateway = Mock()
        compra = Compra(gateway) 
        compra.realiza_compra(3000)
        compra.realiza_compra(2000)
        compra.realiza_compra(5000)
        compra.realiza_compra(6000)
        gateway.recebe_pagamento.assert_assert_has_calls([call(3000), call(2000)])
    
  • assertnotcalled()

    Assegura que o mock NÃO foi chamado durante a execução do teste.

def test_recebe_pagamento_nao_executado(self):
        gateway = Mock()
        compra = Compra(gateway) 
        compra.realiza_compra(null)
        gateway.recebe_pagamento.assert_not_called()
    
  • resetmock(returnvalue=False, side_effect=False)

    Reseta todas as chamadas em um mock. Caso return_value e/ou side_effect sejam True, seu valor de retorno e seu efeito colateral também serão resetados.

def test_recebe_pagamento_reset(self):
        gateway = Mock()
        compra = Compra(gateway) 
        compra.realiza_compra(3000)
        gateway.recebe_pagamento.called #True
        gateway.reset_mock()
        gateway.recebe_pagamento.called #False
    
  • mockaddspec(spec, spec_set=False)

    Adiciona uma coleção de atributos (ou spec) ao mock. spec pode ser um objeto ou uma lista de strings. Caso spec_set seja True os atributos que não estiverem listados em spec serão considerados privados.

def test_get_versao_gateway(self):
        gateway = Mock()
        gateway.mock_add_spec(['version'])
        gateway.version = '1.2'
        compra = Compra(gateway) 
        self.assertEqual(compra.get_versao_gateway(), '1.2')
    
  • attach_mock(mock, atributo) Vincula outro mock como seu próprio atributo, substituindo seu nome e objeto pai
def test_get_versao_gateway(self):
        gateway = Mock()
        versao = Mock(return_value='1.2')
        gateway.attach_mock(versao, 'version')
        compra = Compra(gateway) 
        self.assertEqual(compra.get_versao_gateway(), '1.2')
    
  • configure_mock(kwargs) Configura atributos do mock através de um dicionário com chaves predefinidas.
def test_get_versao_gateway(self):
        gateway = Mock()
        gateway.configure_mock({
            'recebe_pagamento.return_value': True,
        })
        compra = Compra(gateway)
        assertTrue(compra.realiza_compra(3000))
    
  • dir()

    Esse método mágico comumente exibe todos os métodos de dado objeto, no caso de objetos mock, contudo, dir é limitado para resultados relevantes. Para mocks com uma spec isso inclui os atributos permitidos.

Atributos

  • called

    Um boolean que representa se o mock foi chamado ou não

  • call_count

    Um inteiro que retorna quantas vezes o mock foi chamado

  • return_value

    Declara o valor de retorno quando o mock é executado. Por padrão é um novo mock.

  • side_effect

    Pode ser tanto uma função a ser executada quando o mock for chamado, um iterável ou uma exceção (classe ou instância) as ser disparada.

    Caso seja passada uma função ela será chamada com os mesmo argumentos que o mock e, a menos que a função tenha o retorno padrão do mock, o valor de retorno da função será o utilizado.

    Alternativamente, side_effect pode ser uma classe ou instância de exceção. Nese caso uma exception será disparada quando o mock for chamado.

    Caso side_effect seja um iterável então cada chamada resultará no próximo valor do iterável.

    side_effect pode ser setado como None para esvaziá-lo.

  • call_args

    Retorna uma tupla com os argumentos da última chamada o mock ou None caso o mock não tenha sido chamado.

  • callargslist

    Retorna uma lista com todas as chamadas que o mock fez como tuplas dentro da lista. Retorna uma lista vazia caso o mock não tenha nenhuma chamada.

  • method_calls

    Retorna as chamadas de métodos e atributos do mock bem como as do próprio mock.

  • mock_calls

    Retorna todas as chamadas do mock, de seus métodos, atributos, métodos mágicos e valores de retorno.

  • class

    Esse método mágico comumente indica o nome da calsse de dado objeto. Para um objeto mock com um spec, __class__ retornará o tipo do spec, isso permite que mocks passem em testes de isinstance() de classes que eles estão substituindo.

    __class__ é atribuível, isso permite que o mock passe em testes isinstance() ou sejam passados em um parâmetro que exige certo tipo sem a necessidade de usar um spec.

def test_realiza_compra(self):
        gateway = Mock()
        gateway.__class__ = Gateway
        isinstance(gateway, Gateway) #True
    

Mock de métodos mágicos

Mock permite a mockagem dos métodos de protocolo do Python, também conhecidos como "métodos mágicos". Isso permite que objetos mock substituam contenders ou outros objetos que implementam protocolos Python. Como métodos mágicos são vistos de forma diferente dos demais métodos essa funcionalidade foi implementada especificamente. Portanto, não são todos os métodos mágicos suportados, apesar da lista incluir a maioria deles.

Podemos mockar métodos mágicos definindo o método desejado para uma função ou objeto mock. Caso seja uma função então o primeiro argumento precisa ser self:

def __str__(self, *args, **kwargs):
        return "string de testes"
    
    def test_stringficador(self):
        gateway = Mock()
        gateway.__str__ = self.__str__
        self.assertEqual(str(gateway), "string de testes")
    
Também é possível mockar esse método mágico da seguinte forma:
def test_stringficador(self):
        gateway = Mock()
        gateway.__str__ = Mock(return_value = "string de testes")
        self.assertEqual(str(gateway), "string de testes")
    

Chamadas a métodos mágicos não aparecem em method_calls mas aparecem em mock_calls.

Nota: Se a palavra-chave spec foi usada na criação do Mock como argumento então definir um método mágico que não esteja na spec resultará em um erro do tipo AtributeError

Abaixo a lista completa dos métodos mágicos suportados.

  • __hash__, __sizeof__, __repr__, __str__
  • __dir__, __format__, __subclasses__
  • __floor__, __trunc__, __ceil__
  • Comparações: __lt__, __gt__, __le__, __ge__, __eq__, __ne__
  • Métodos Contenders: __getitem__, __setitem__, __delitem__, __contains__, __len__, __iter__, __reversed__, __missing__
  • Gerenciador de contexto: __enter__, __exit__
  • Métodos numéricos unários: __neg__, __pos__, __invert__
  • Os métodos numérios (incluindo mão direita, variantes no local): __add__ , __sub__, __mul__, __matmul__, __div__, __truediv__, __floordiv__, __mod__ __divmod__, __lshift__, __rshift__, __e__, __xor__, __or__ ,, __pow__
  • Métodos de conversão numérica: __complex__, __int__, __float__, __index__
  • Métodos descritores: __get__, __set__, __delete__
  • Pickling: __reduce__, __reduce_ex__, __getinitargs__, __getnewargs__, __getstate__, __setstate__

Os seguintes métodos existem porem não estão disponíveis por serem usados pelo módulo mock, são definidos dinamicamente, ou podem causar problemas

  • __getattr__, __setattr__, __init__ , __new__
  • __prepare__, __instancecheck__, __subclasscheck__, __del__

A classe MagicMock

Existem duas variantes de MagicMock: MagicMock e NonCallableMagicMock

class unittest.mock.MagicMock(*args, kwargs)

MagicMock é uma subclasse de Mock com implementações da maioria dos métodos mágicos pré configuradas por padrão. Os parâmetros do construtor possuem o mesmo significado que Mock

Caso spec ou spec_set seja usado então apenas métodos mágicos definidos no spec serão criados.

class unittest.mock.NonCallableMagicMock(*args, kwargs)

Uma versão de MagicMock que não pode ser chamada.

Os parâmetros do construtor possuem o mesmo significado que MagicMock com exceção de return_value e side_effect que não tem significado em um objeto que não pode ser chamado.

Os métodos mágicos são configurados com objetos MagicMock, portanto é possível configurá-los e usá-los da maneira usual:

def test_get_item(self):
        lista = MagicMock()
        lista.__getitem__.return_value = 42
        lista[3] = "quarenta e dois"
        self.assertEqual(lista[3], 42)
    

Note acima que não importa qualquer atribuição feita em qualquer índice da lista, já que o valor de retorno de um item da lista foi definido para sempre ser 42.

Muitos métodos de protocolo retornam por padrão objetos de um tipo específico. Portanto para que possam ser usados sem que seja necessário configurar um valor de retorno, esses métodos são pré configurados com um retorno padrão. Ainda é possível configurar manualmente o valor de retorno caso o padrão não seja adequado.

Abaixo os métodos e seus retornos:

  • __lt__: NotImplemented
  • __gt__: NotImplemented
  • __le__: NotImplemented
  • __ge__: NotImplemented
  • __int__: 1
  • __contains__: False
  • __len__: 0
  • __iter__: iter([ ])
  • __exit__: False
  • __complex__: 1j
  • __float__: 1.0
  • __bool__: True
  • __index__: 1
  • __hash__: hash padrão do mock
  • __str__: str padrão do mock
  • __sizeof__: sizeof padrão do mock

O módulo patch

Os decorators patch são usados para "remendar" (do inglês patch) objetos no escopo da função eles decoram. Esses decorators desfazem seu próprio uso automaticamente e tratam exceções potencialmente disparadas durante esse processo. Todas essas funções podem ser usadas em um with ou como decorators de classe.

unittest.mock.patch(alvo, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, kwargs)
@patch('modulo.Classe3')
    @patch('modulo.Classe2')
    @patch('modulo.Classe1')
    def test(ClasseFalsa1, ClasseFalsa2, ClasseFalsa3):
        assert ClasseFalsa1 is modulo.Classe1
        assert ClasseFalsa2 is modulo.Classe2
        assert ClasseFalsa3 is modulo.Classe3
    

Nota: Quando mais de um decorator de patch é anotado sobre dada função com dois ou mais argumentos os mocks serão aplicados na ordem normal do python de aplicação de decorators, ou seja, começando de baixo para cima. No exemplo abaixo o mock para modulo.Classe1 será passado em ClasseFalsa1 e assim por diante. Com patch é importante mockar objetos namespace onde eles são procurados.

Além de decorator o patch também pode agir como gerenciador de contexto em uma expressão with

with patch("modulo.Classe1") as ClasseFalsa1:
        assert modulo.Classe1 is ClasseFalsa1
    

Assertions

São os métodos que fazem o teste propriamente dito, são os que executam a unidade e certificam que o resultado gerado no teste é o esperado com sucesso ou falham.

Visão Geral

Os métodos da classe TestCase que começam com o prefixo assert, são parte importante dos casos de teste e avaliam se certas condições, decisivas para determinar o sucesso ou falha do teste, geram um resultado esperado.

a
...
class FuncionarioTest(unittest.TestCase):
    ...
    def test_salario_liquido(self):
        funcionario = Funcionario(2500)
        self.assertEqual(2026.43, funcionario.salario_liquido(), msg="salário real difere do esperado")
    ...

Métodos Assert

assertEqual()

Testa se first e second possuem valores iguais.

assertEqual(fisrt, second, msg=None)

assertNotEqual

Testa se first e second não possuem valores iguais.

assertNotEqual(first, second, msg=None)

assertTrue

Testa se bool(expr) é igual a True.

assertTrue(expr, msg=None)

assertFalse() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertIs

Testa se first e second são idênticos em valor e tipo.

assertIs(first, second, msg=None)

assertIsNot() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertIsNone

Testa se expr é igual a None.

assertIsNone(expr, msg=None)

assertIsNotNone() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertIn

Testa se first está contido second.

assertIn(first, second, msg=None)

assertNotIn() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertIsInstance

Testa se obj é uma instância de cls, que pode ser uma classe ou tupla de classes.

assertIsInstance(obj, cls, msg=None)

assertNotIsInstance() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertRaises

Testa se o código sendo testado irá gerar uma exceção. Pode ser usada recebendo um código executável (uma função) ou como um gerenciador de contexto.

assertRaises(exception, callable, *args, **kwargs)

Testa se uma exception é disparada quando callable é executada com quaisquer parâmetros posicionais ou de palavras-chave. O teste falha se uma exceção de algum outro tipo que não a informada é dispara ou se nenhuma é. Para detectar um grupo de exceções, uma tupla contendo as classes das exceções pode ser passada em exception.

assertRaises(exception, msg=None)

Retorna um gerenciador de contexto que vai guardar a exceção em seu atributo exception e pode ser usado para fazer verificações adicionais na exceção:

with self.assertRaises(SomeException) as cm:
    do_something()
    the_exception = cm.exception
    self.assertEqual(the_exception.error_code, 3) 

assertRaisesRegex

Assim como assertRaises, verifica se o código sendo testado irá gerar uma exceção, porém também irá testar se a string regex combina com a string gerada pela exceção.

assertRaisesRegex(exception, regex, callable, *args, **kwargs)

Quando usado passando um código chamável em callable

assertRaisesRegex(exception, regex, msg=None)

Quando usado como gerenciador de contexto

assertWarns

Testa se o código sendo testado irá gerar um warning. Pode ser usada recebendo um código executável (uma função) ou como um gerenciador de contexto.

assertWarns(warning, callable, *args, **kwargs)

Testa se um warning é disparada quando callable é executada com quaisquer parâmetros posicionais ou de palavras-chave. O teste falha se um aviso de algum outro tipo que não a informada é dispara ou se nenhuma é. Para detectar um grupo de exceções, uma tupla contendo as classes das exceções pode ser passada em warning.

assertWarns(warning, msg=None)

Retorna um gerenciador de contexto que vai guardar o aviso em seu atributo warning e pode ser usado para fazer verificações adicionais no aviso:

with self.assertWarns(SomeWarning) as cm:
        do_something()
        the_warning = cm.warning
        self.assertEqual(the_warning.error_code, 3)
        

assertWarnsRegex

Assim como assertWarns, verifica se o código sendo testado irá gerar uma exceção, porém também irá testar se a string regex combina com a string gerada pela warning.

assertWarnsRegex(warning, regex, callable, *args, **kwargs)

Faz o mesmo que assertWarns() porém também testa se a string regex combina com a string gerado pelo aviso

assertWarnsRegex(warning, regex, msg=None)

Faz o mesmo que assertWarns() porém também testa se a string regex combina com a string gerado pelo aviso

assertLogs

Um gerenciador de contexto que testa se ao menos uma mensagem é enviada ao logger ou a um de seus filhos no dado level.

assertLogs(logger=None, level=None)
        

Por padrão o logger é o root, que captura todas as mensagens. Caso informado, o logger precisa ser um objeto loggin.Logger ou uma string com o nome do logger.

Caso definido, level precisa ser um número para o nível de loggin ou uma string equivalente (por exemplo "ERROR" ou logging.ERROR). O padrão é logging.INFO.

O teste passa se ao menos uma mensagem emitida dentro do bloco with é compatível com as condições logger e level, caso contrário o teste falha.

O objeto retornado pelo gerenciador de contexto é um helper de gravação que mantem observa as correspondentes mensagens de log. Possui dois atributos.

  • records

    Uma lista de objetos do tipo loggin.LogRecord das mensagens do log correspondentes

  • output

    Uma lista de objetos str com as mensagens de saída correspondentes formatadas

    Exemplo:

    with self.assertLogs('foo', level='INFO') as cm:
                    logging.getLogger('foo').info('first message')
                    logging.getLogger('foo.bar').error('second message')
                    self.assertEqual(cm.output, ['INFO:foo:first message', 'ERROR:foo.bar:second message'])
                    

assertAlmostEqual

Testa se first e second são aproximados em igualdade computando a diferença, arredondando essa diferença em um dado número de casa decimais (padrão 7) e a comparando a zero. note que esse método arredonda o número em casa decimais (como a função round()) e não dígitos significativos.

assertAlmostEqual(first, second, places=7, msg=None, delta=None)

Caso delta seja definido ao invés de places, então a diferença entre first e second precisa ser menor ou igual a delta.

Caso ambos delta e places sejam definidos um TypeError será disparado.

assertNotAlmostEqual() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertGreater

Testa se primeiro for maior que segundo, retorna uma falha se não for.

assertGreater(first, second, msg=None)

assertGreaterEqual

Testa se primeiro for maior ou igual ao segundo, retorna uma falha se não for.

assertGreaterEqual(first, second, msg=None)

assertLess

Testa se primeiro for menor que segundo, retorna uma falha se não for.

assertLess(first, second, msg=None)

assertLessEqual

Testa se primeiro for menor ou igual ao segundo, retorna uma falha se não for.

assertLessEqual(first, second, msg=None)

assertRegex

Testa se regex combina com alguma parte de text, retorna uma falha caso nenhuma parte combine.

assertRegex(text, regex, msg=None)

assertNotRegex() corresponde ao método assert inverso e recebe os mesmos parâmetros.

assertCountEqual

Testa se a sequencia first contem o mesmo número de elementos de second independente da ordem. Caso não possua uma mensagem de erro listando as diferenças será gerada.

assertCountEqual(first, second, msg=None)

Elementos duplicados não são ignorados quando comparando first e second. Verifica se cada elemento tem a mesma contagem em ambas as sequencias. Equivalente a assertEqual(Counter(list(first)), Counter(list(second))) mas funciona com sequencia de objetos unhashable.

assertMultiLineEqual

Testa se a string multilinha first é igual a string second. Quando não são a diferença entre as duas strings será destacada e incluída na mensagem de falha. Esse método é executado por padrão quando strings são passadas no assertEqual()

assertMultiLineEqual(first, second, msg=None)

assertSequenceEqual

Testa se sequencia first é igual a sequencia second. caso seq_type seja definido, ambos first e second precisam ser instâncias de seq_type ou o teste falhará. Se as sequências forem diferentes uma mensagem de erro será construída com a diferença entre as duas.

assertSequenceEqual(first, second, msg=None, seq_type=None)

Esse método não é chamado diretamente por assertEqual() mas é implementado por assertListEqual() e assertTupleEqual().

assertListEqual

Testa se a lista first é igual a second, caso não sejam uma mensagem de erro é gerada com a diferença entre as duas. Um erro será gerado caso first e second não sejam do tipo list.

assertListEqual(first, second, msg=None)

assertTupleEqual

Testa se a tupla first é igual a second, caso não sejam uma mensagem de erro é gerada com a diferença entre as duas. Um erro será gerado caso first e second não sejam do tipo tuple.

assertTupleEqual(first, second, msg=None)

assertSetEqual

Testa se o set first é igual ao second. Caso não sejam, uma mensagem de erro é gerada destacando as diferenças entre os dois sets. O teste falha se ambos first ou second não possuem o método set.difference().

assertSetEqual(first, second, msg=None)

assertDictEqual

Testa se o dicionário first é igual ao second. Caso não sejam, uma mensagem de erro é gerada destacando a diferença entre os dois dicionários.

assertDictEqual(first, second, msg=None)