Come ottengo il naso per scoprire casi di test generati dinamicamente?
-
19-08-2019 - |
Domanda
Questo è il seguito di un domanda precedente mia.
Nella domanda precedente, sono stati esplorati metodi per implementare quello che era essenzialmente lo stesso test su un'intera famiglia di funzioni, assicurando che i test non si fermassero alla prima funzione fallita.
La mia soluzione preferita ha usato una metaclasse per inserire dinamicamente i test in unittest.TestCase. Sfortunatamente, nose non lo rileva perché il naso esegue la scansione statica dei casi di test.
Come ottengo il naso per scoprire ed eseguire un TestCase? Fare riferimento qui per un esempio del TestCase in questione.
Soluzione
Il naso ha un "generatore di test" caratteristica per cose come questa. Scrivi una funzione del generatore che produce ogni "caso di test" funzione che vuoi che funzioni, insieme ai suoi argomenti. Seguendo il tuo esempio precedente, questo potrebbe controllare ciascuna delle funzioni in un test separato:
import unittest
import numpy
from somewhere import the_functions
def test_matrix_functions():
for function in the_functions:
yield check_matrix_function, function
def check_matrix_function(function)
matrix1 = numpy.ones((5,10))
matrix2 = numpy.identity(5)
output = function(matrix1, matrix2)
assert matrix1.shape == output.shape, \
"%s produces output of the wrong shape" % str(function)
Altri suggerimenti
Nose non esegue la scansione statica dei test, quindi puoi usare la magia della metaclasse per fare i test che Nose trova.
La parte difficile è che le tecniche di metaclasse standard non impostano correttamente l'attributo func_name, che è ciò che Nose cerca quando controlla se i metodi della tua classe sono test.
Ecco una semplice metaclasse. Esamina il funzionamento e aggiunge un nuovo metodo per ogni metodo che trova, affermando che il metodo trovato ha una dotstring. A questi nuovi metodi sintetici vengono dati i nomi "test_% d" % I
.
import new
from inspect import isfunction, getdoc
class Meta(type):
def __new__(cls, name, bases, dct):
newdct = dct.copy()
for i, (k, v) in enumerate(filter(lambda e: isfunction(e[1]), dct.items())):
def m(self, func):
assert getdoc(func) is not None
fname = 'test_%d' % i
newdct[fname] = new.function(m.func_code, globals(), fname,
(v,), m.func_closure)
return super(Meta, cls).__new__(cls, 'Test_'+name, bases, newdct)
Ora creiamo una nuova classe che utilizza questa metaclasse
class Foo(object):
__metaclass__ = Meta
def greeter(self):
"sdf"
print 'Hello World'
def greeter_no_docstring(self):
pass
In fase di esecuzione, Foo
sarà effettivamente chiamato Test_Foo
e avrà greeter
, greeter_no_docstring
, test_1
e test_2
come metodi. Quando eseguo nosetests
su questo file, ecco l'output:
$ nosetests -v test.py
test.Test_Foo.test_0 ... FAIL
test.Test_Foo.test_1 ... ok
======================================================================
FAIL: test.Test_Foo.test_0
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Library/Frameworks/EPD64.framework/Versions/7.3/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
self.test(*self.arg)
File "/Users/rmcgibbo/Desktop/test.py", line 10, in m
assert getdoc(func) is not None
AssertionError
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)
Questa metaclasse non è davvero utile così com'è, ma se invece usi il Meta
non come una metaclasse corretta, ma come più di una metaclasse funzionale (ovvero accetta una classe come argomento e restituisce una nuova classe, quella che è stata rinominata in modo che nose possa trovarla), quindi è utile. Ho usato questo approccio per testare automaticamente che i docstring aderiscono allo standard Numpy come parte di una suite di test del naso.
Inoltre, ho avuto molti problemi a far funzionare correttamente la chiusura con new.function, motivo per cui questo codice utilizza m (self, func)
dove func
è fatto per essere un argomento predefinito. Sarebbe più naturale usare una chiusura su value
, ma ciò non sembra funzionare.
Potresti provare a generare le classi testcase con type ()
class UnderTest_MixIn(object):
def f1(self, i):
return i + 1
def f2(self, i):
return i + 2
SomeDynamicTestcase = type(
"SomeDynamicTestcase",
(UnderTest_MixIn, unittest.TestCase),
{"even_more_dynamic":"attributes .."}
)
# or even:
name = 'SomeDynamicTestcase'
globals()[name] = type(
name,
(UnderTest_MixIn, unittest.TestCase),
{"even_more_dynamic":"attributes .."}
)
Questo dovrebbe essere creato quando nose tenta di importare il tuo test_module, quindi dovrebbe funzionare.
Il vantaggio di questo approccio è che puoi creare molte combinazioni di test in modo dinamico.