Teste de unidade Python com base e subclasse

148

Atualmente, tenho alguns testes de unidade que compartilham um conjunto comum de testes. Aqui está um exemplo:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

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

A saída do acima é:

Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

Existe uma maneira de reescrever o que testCommonfoi dito acima para que o primeiro não seja chamado?

Edição: Em vez de executar 5 testes acima, eu quero executar apenas 4 testes, 2 do SubTest1 e outros 2 do SubTest2. Parece que o Python unittest está executando o BaseTest original por conta própria e preciso de um mecanismo para impedir que isso aconteça.

Thierry Lam
fonte
Vejo que ninguém mencionou, mas você tem a opção de alterar a parte principal e executar um conjunto de testes que possui todas as subclasses do BaseTest?
Kon psych

Respostas:

154

Use herança múltipla, para que sua classe com testes comuns não herda de TestCase.

import unittest

class CommonTests(object):
    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(unittest.TestCase, CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(unittest.TestCase, CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()
Matthew Marshall
fonte
1
Essa é a solução mais elegante até agora.
Thierry Lam
27
Esse método funciona apenas para os métodos setUp e tearDown se você reverter a ordem das classes base. Como os métodos são definidos em unittest.TestCase e não chamam super (), qualquer método setUp e tearDown em CommonTests precisa ser o primeiro no MRO ou não será chamado.
111110 Ian Clelland
32
Só para esclarecer o comentário de Ian Clelland para que ele vai ser mais clara para pessoas como eu: se você adicionar setUpe tearDownmétodos para CommonTestsclasse, e você quer que eles sejam chamados para cada teste em classes derivadas, você tem que inverter a ordem das classes de base, de modo que ele vai ser: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov 17/07/2013
6
Eu realmente não sou fã dessa abordagem. Isso estabelece um contrato no código que as classes devem herdar de ambos unittest.TestCase e CommonTests . Eu acho que o setUpClassmétodo abaixo é o melhor e é menos propenso a erros humanos. Isso ou agrupar a classe BaseTest em uma classe de contêiner que é um pouco mais invasiva, mas evita a mensagem de ignorar na impressão da execução de teste.
David Sanders
10
O problema com este é que o pylint se encaixa porque CommonTestsestá invocando métodos que não existem nessa classe.
MadScientist
145

Não use herança múltipla, ela o morderá mais tarde .

Em vez disso, você pode simplesmente mover sua classe base para o módulo separado ou envolvê-la com a classe em branco:

class BaseTestCases:

    class BaseTest(unittest.TestCase):

        def testCommon(self):
            print('Calling BaseTest:testCommon')
            value = 5
            self.assertEqual(value, 5)


class SubTest1(BaseTestCases.BaseTest):

    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTestCases.BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

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

A saída:

Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK
Vadim P.
fonte
6
Esse é meu favorito. É o meio menos invasivo e não interfere nos métodos de substituição, não altera o MRO e me permite definir setUp, setUpClass etc. na classe base.
Hannes
6
Eu seriamente não entendo (de onde vem a magia vem?), Mas é longe a melhor solução de acordo com me :) Vindo de Java, eu odeio herança múltipla ...
Edouard Berthe
4
O @Edouardb unittest executa apenas classes no nível do módulo que herdam do TestCase. Mas o BaseTest não está no nível do módulo.
joshb
Como uma alternativa muito semelhante, você pode definir o ABC dentro de uma função no-args que retorna o ABC quando chamado
Anakhand
34

Você pode resolver esse problema com um único comando:

del(BaseTest)

Portanto, o código ficaria assim:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

del(BaseTest)

if __name__ == '__main__':
    unittest.main()
Wojciech B.
fonte
3
BaseTest é um membro do módulo enquanto está sendo definido, portanto está disponível para uso como a classe base dos SubTestes. Pouco antes da definição ser concluída, del () a remove como membro, para que a estrutura mais unida não a encontre quando procurar subclasses TestCase no módulo.
mhsmith
3
Esta é uma resposta incrível! Gosto mais do que o de @MatthewMarshall, porque em sua solução, você obtém erros de sintaxe do pylint, porque os self.assert*métodos não existem em um objeto padrão.
SimplyKnownAsG
1
Não funciona se BaseTest for referenciado em qualquer outro lugar na classe base ou em suas subclasses, por exemplo, ao chamar super () no método substitui: super( BaseTest, cls ).setUpClass( )
Hannes
1
@ Hannes Pelo menos no python 3, BaseTestpode ser referenciado pelas subclasses super(self.__class__, self)ou apenas super()nas subclasses, embora aparentemente não se você fosse herdar construtores . Talvez exista também uma alternativa "anônima" quando a classe base precisar fazer referência a si mesma (não que eu tenha alguma idéia de quando uma classe precisar fazer referência a si mesma).
Stein
28

A resposta de Matthew Marshall é ótima, mas requer que você herde de duas classes em cada um dos seus casos de teste, o que é propenso a erros. Em vez disso, eu uso isso (python> = 2.7):

class BaseTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        if cls is BaseTest:
            raise unittest.SkipTest("Skip BaseTest tests, it's a base class")
        super(BaseTest, cls).setUpClass()
Dennis Golomazov
fonte
3
Isso é legal. Existe uma maneira de se locomover tendo que pular? Para mim, os pulos são indesejáveis ​​e são usados ​​para indicar um problema no plano de teste atual (com o código ou com o teste)?
Zach Young
@ZacharyYoung Não sei, talvez outras respostas possam ajudar.
Dennis Golomazov
@ZacharyYoung Tentei corrigir esse problema, veja minha resposta.
21614 simonzack
não está claro imediatamente o que é inerentemente propenso a erros sobre herdar de duas classes
GTC
@jwg veja comentários para a resposta aceita :) Você precisa herdar cada uma das suas classes de teste das duas classes base; você precisa preservar a ordem correta deles; se você quiser adicionar outra classe de teste base, também precisará herdar dela. Não há nada errado com os mixins, mas, neste caso, eles podem ser substituídos por um simples salto.
Dennis Golomazov
7

O que você está tentando alcançar? Se você tiver um código de teste comum (asserções, testes de gabarito etc.), coloque-os em métodos que não sejam prefixados testpara unittestnão carregá-los.

import unittest

class CommonTests(unittest.TestCase):
      def common_assertion(self, foo, bar, baz):
          # whatever common code
          self.assertEqual(foo(bar), baz)

class BaseTest(CommonTests):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)

class SubTest2(CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()
John Millikin
fonte
1
Sob sua sugestão, common_assertion () ainda seria executado automaticamente ao testar as subclasses?
23415 Stewart
@ Stewart Não, não seria. A configuração padrão é executar apenas métodos começando com "test".
CS
6

A resposta de Matthew é a que eu precisava usar, pois ainda estou com 2,5. Mas a partir da versão 2.7, você pode usar o decorador @ unittest.skip () em qualquer método de teste que você queira pular.

http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures

Você precisará implementar seu próprio decorador de pulos para verificar o tipo de base. Não usei esse recurso antes, mas em cima da minha cabeça você poderia usar o BaseTest como marcador tipo de para condicionar o salto:

def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func
Jason A
fonte
5

Uma maneira de pensar em resolver isso é ocultando os métodos de teste se a classe base for usada. Dessa forma, os testes não são ignorados; portanto, os resultados podem ser verdes em vez de amarelos em muitas ferramentas de relatórios de testes.

Comparado ao método mixin, ide como o PyCharm não vai reclamar que os métodos de teste de unidade estão faltando na classe base.

Se uma classe base herdar dessa classe, ela precisará substituir os métodos setUpClasse tearDownClass.

class BaseTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls._test_methods = []
        if cls is BaseTest:
            for name in dir(cls):
                if name.startswith('test') and callable(getattr(cls, name)):
                    cls._test_methods.append((name, getattr(cls, name)))
                    setattr(cls, name, lambda self: None)

    @classmethod
    def tearDownClass(cls):
        if cls is BaseTest:
            for name, method in cls._test_methods:
                setattr(cls, name, method)
            cls._test_methods = []
simonzack
fonte
5

Você pode adicionar __test_ = Falsena classe BaseTest, mas, se adicionar, lembre-se de que deve adicionar __test__ = Trueclasses derivadas para poder executar testes.

import unittest

class BaseTest(unittest.TestCase):
    __test__ = False

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):
    __test__ = True

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    __test__ = True

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

if __name__ == '__main__':
    unittest.main()
peja
fonte
4

Outra opção é não executar

unittest.main()

Em vez disso, você pode usar

suite = unittest.TestLoader().loadTestsFromTestCase(TestClass)
unittest.TextTestRunner(verbosity=2).run(suite)

Então você só executa os testes na classe TestClass

Anjo
fonte
Esta é a solução menos hacky. Em vez de modificar o que é unittest.main()coletado no conjunto padrão, você forma um conjunto explícito e executa seus testes.
Zgoda # 26/17
1

Eu fiz o mesmo que @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ), mas modifiquei um pouco:

import unittest2


from some_module import func1, func2


def make_base_class(func):

    class Base(unittest2.TestCase):

        def test_common1(self):
            print("in test_common1")
            self.assertTrue(func())

        def test_common2(self):
            print("in test_common1")
            self.assertFalse(func(42))

    return Base



class A(make_base_class(func1)):
    pass


class B(make_base_class(func2)):

    def test_func2_with_no_arg_return_bar(self):
        self.assertEqual("bar", func2())

e lá vamos nós.

gst
fonte
1

No Python 3.2, você pode adicionar uma função test_loader a um módulo para controlar quais testes (se houver) são encontrados pelo mecanismo de descoberta de testes.

Por exemplo, o seguinte carregará apenas os pôsteres SubTest1e SubTest2casos de teste originais , ignorando Base:

def load_tests(loader, standard_tests, pattern):
    suite = TestSuite()
    suite.addTests([SubTest1, SubTest2])
    return suite

Deveria ser possível iterar standard_tests(a TestSuitecontendo os testes encontrados pelo carregador padrão) e copiar tudo, exceto Basepara suite, mas a natureza aninhada TestSuite.__iter__torna isso muito mais complicado.

jbosch
fonte
0

Apenas renomeie o método testCommon para outra coisa. O Unittest (geralmente) pula qualquer coisa que não tenha 'teste'.

Rápido e simples

  import unittest

  class BaseTest(unittest.TestCase):

   def methodCommon(self):
       print 'Calling BaseTest:testCommon'
       value = 5
       self.assertEquals(value, 5)

  class SubTest1(BaseTest):

      def testSub1(self):
          print 'Calling SubTest1:testSub1'
          sub = 3
          self.assertEquals(sub, 3)


  class SubTest2(BaseTest):

      def testSub2(self):
          print 'Calling SubTest2:testSub2'
          sub = 4
          self.assertEquals(sub, 4)

  if __name__ == '__main__':
      unittest.main()`
Kashif Siddiqui
fonte
2
Isso resultaria na não execução do teste methodCommon em qualquer um dos SubTestes.
Pepper Lebeck-Jobe
0

Portanto, esse é um tipo de tópico antigo, mas me deparei com esse problema hoje e pensei em meu próprio truque. Ele usa um decorador que torna os valores das funções None quando acessados ​​através da classe base. Não precisa se preocupar com setup e setupclass porque, se a classe base não tiver testes, ela não será executada.

import types
import unittest


class FunctionValueOverride(object):
    def __init__(self, cls, default, override=None):
        self.cls = cls
        self.default = default
        self.override = override

    def __get__(self, obj, klass):
        if klass == self.cls:
            return self.override
        else:
            if obj:
                return types.MethodType(self.default, obj)
            else:
                return self.default


def fixture(cls):
    for t in vars(cls):
        if not callable(getattr(cls, t)) or t[:4] != "test":
            continue
        setattr(cls, t, FunctionValueOverride(cls, getattr(cls, t)))
    return cls


@fixture
class BaseTest(unittest.TestCase):
    def testCommon(self):
        print('Calling BaseTest:testCommon')
        value = 5
        self.assertEqual(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

if __name__ == '__main__':
    unittest.main()
Nathan Buckner
fonte
-2

Altere o nome do método BaseTest para setUp:

class BaseTest(unittest.TestCase):
    def setUp(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

Resultado:

Realizou 2 testes em 0.000s

Chamando BaseTest: testCommon Chamando
SubTest1: testSub1 Chamando
BaseTest: testCommon Chamando
SubTest2: testSub2

A partir da documentação :


Método TestCase.setUp () chamado para preparar o dispositivo de teste. Isso é chamado imediatamente antes de chamar o método de teste; qualquer exceção gerada por esse método será considerada um erro e não uma falha no teste. A implementação padrão não faz nada.

Brian R. Bondy
fonte
Isso funcionaria, e se eu tiver n testCommon, devo colocá-los todos em baixo setUp?
Thierry Lam
1
Sim, você deve colocar todo o código que não é um caso de teste real em setUp.
Brian R. Bondy
Porém, se uma subclasse possui mais de um test...método, setUpé executada repetidamente, uma vez por esse método; portanto, não é uma boa ideia colocar testes lá!
287 Alex Martelli
Não tenho muita certeza do que o OP queria em termos de quando executado em um cenário mais complexo.
Brian R. Bondy