Come affermare l'output con nosetest / unittest in python?


114

Sto scrivendo test per una funzione come la prossima:

def foo():
    print 'hello world!'

Quindi, quando voglio testare questa funzione, il codice sarà così:

import sys
from foomodule import foo
def test_foo():
    foo()
    output = sys.stdout.getline().strip() # because stdout is an StringIO instance
    assert output == 'hello world!'

Ma se eseguo nosetest con il parametro -s il test va in crash. Come posso catturare l'output con il modulo unittest o nose?


Risposte:


125

Uso questo gestore di contesto per acquisire l'output. Alla fine utilizza la stessa tecnica di alcune delle altre risposte sostituendole temporaneamente sys.stdout. Preferisco il gestore di contesto perché racchiude tutta la contabilità in una singola funzione, quindi non devo riscrivere alcun codice di prova finale e non devo scrivere funzioni di configurazione e smontaggio solo per questo.

import sys
from contextlib import contextmanager
from StringIO import StringIO

@contextmanager
def captured_output():
    new_out, new_err = StringIO(), StringIO()
    old_out, old_err = sys.stdout, sys.stderr
    try:
        sys.stdout, sys.stderr = new_out, new_err
        yield sys.stdout, sys.stderr
    finally:
        sys.stdout, sys.stderr = old_out, old_err

Usalo in questo modo:

with captured_output() as (out, err):
    foo()
# This can go inside or outside the `with` block
output = out.getvalue().strip()
self.assertEqual(output, 'hello world!')

Inoltre, poiché lo stato di output originale viene ripristinato all'uscita dal withblocco, possiamo impostare un secondo blocco di cattura nella stessa funzione del primo, che non è possibile utilizzando le funzioni di configurazione e smontaggio, e diventa prolisso durante la scrittura di try-latest blocchi manualmente. Questa capacità è tornata utile quando l'obiettivo di un test era confrontare i risultati di due funzioni l'una rispetto all'altra piuttosto che a un valore precalcolato.


Questo ha funzionato molto bene per me in pep8radius . Recentemente, tuttavia, l'ho usato di nuovo e ho ricevuto il seguente errore durante la stampa TypeError: unicode argument expected, got 'str'(il tipo passato a print (str / unicode) è irrilevante).
Andy Hayden

9
Hmmm può essere che in Python 2 vogliamo from io import BytesIO as StringIOe in Python 3 solo from io import StringIO. Sembrava risolvere il problema nei miei test, credo.
Andy Hayden

4
Ooop, solo per finire, scusa per così tanti messaggi. Giusto per chiarire per le persone che trovano questo: python3 usa io.StringIO, python 2 usa StringIO.StringIO! Grazie ancora!
Andy Hayden

Perché tutti gli esempi qui richiamano strip()i unicoderitornati da StringIO.getvalue()?
Palimondo

1
No, @Vedran. Ciò si basa sul riassociare il nome a cui appartiene sys. Con l'istruzione import, stai creando una variabile locale denominata stderrche ha ricevuto una copia del valore in sys.stderr. Le modifiche a una non si riflettono nell'altra.
Rob Kennedy,

60

Se vuoi davvero farlo, puoi riassegnare sys.stdout per la durata del test.

def test_foo():
    import sys
    from foomodule import foo
    from StringIO import StringIO

    saved_stdout = sys.stdout
    try:
        out = StringIO()
        sys.stdout = out
        foo()
        output = out.getvalue().strip()
        assert output == 'hello world!'
    finally:
        sys.stdout = saved_stdout

Se dovessi scrivere questo codice, tuttavia, preferirei passare un outparametro opzionale alla foofunzione.

def foo(out=sys.stdout):
    out.write("hello, world!")

Quindi il test è molto più semplice:

def test_foo():
    from foomodule import foo
    from StringIO import StringIO

    out = StringIO()
    foo(out=out)
    output = out.getvalue().strip()
    assert output == 'hello world!'

11
Nota: in python 3.x la StringIOclasse ora deve essere importata dal iomodulo. from io import StringIOfunziona in Python 2.6+.
Bryan P

2
Se usi from io import StringIOin Python 2, ottieni un file TypeError: unicode argument expected, got 'str'durante la stampa.
matiasg

9
Nota rapida: in python 3.4, puoi utilizzare il gestore di contesto contextlib.redirect_stdout per farlo in un modo sicuro rispetto alle eccezioni:with redirect_stdout(out):
Lucretiel

2
Non devi farlo saved_stdout = sys.stdout, hai sempre un riferimento magico a questo sys.__stdout__, ad esempio, hai solo bisogno sys.stdout = sys.__stdout__di pulire.
ThorSummoner

@ThorSummoner Grazie, questo ha solo semplificato alcuni dei miei test ... per le immersioni che vedo che hai recitato .... piccolo mondo!
Jonathon Reinhart

48

Dalla versione 2.7, non è più necessario riassegnare sys.stdout, questo viene fornito tramite bufferflag . Inoltre, è il comportamento predefinito di nosetest.

Ecco un esempio di errore in un contesto non bufferizzato:

import sys
import unittest

def foo():
    print 'hello world!'

class Case(unittest.TestCase):
    def test_foo(self):
        foo()
        if not hasattr(sys.stdout, "getvalue"):
            self.fail("need to run in buffered mode")
        output = sys.stdout.getvalue().strip() # because stdout is an StringIO instance
        self.assertEquals(output,'hello world!')

È possibile impostare tampone attraverso la unit2linea di comando di bandiera -b, --buffero in unittest.mainopzioni. L'opposto si ottiene attraverso la nosetestbandiera --nocapture.

if __name__=="__main__":   
    assert not hasattr(sys.stdout, "getvalue")
    unittest.main(module=__name__, buffer=True, exit=False)
    #.
    #----------------------------------------------------------------------
    #Ran 1 test in 0.000s
    #
    #OK
    assert not hasattr(sys.stdout, "getvalue")

    unittest.main(module=__name__, buffer=False)
    #hello world!
    #F
    #======================================================================
    #FAIL: test_foo (__main__.Case)
    #----------------------------------------------------------------------
    #Traceback (most recent call last):
    #  File "test_stdout.py", line 15, in test_foo
    #    self.fail("need to run in buffered mode")
    #AssertionError: need to run in buffered mode
    #
    #----------------------------------------------------------------------
    #Ran 1 test in 0.002s
    #
    #FAILED (failures=1)

Nota che questo interagisce con --nocapture; in particolare, se questo flag è impostato, la modalità bufferizzata sarà disabilitata. Quindi hai la possibilità di vedere l'output sul terminale o di provare che l'output è come previsto.
ijoseph

1
È possibile attivarlo e disattivarlo per ogni test, perché questo rende il debug molto difficile quando si usa qualcosa come ipdb.set_trace ()?
Lqueryvg

33

Molte di queste risposte non sono riuscite per me perché non è possibile from StringIO import StringIOin Python 3. Ecco uno snippet minimo di lavoro basato sul commento di @ naxa e sul Python Cookbook.

from io import StringIO
from unittest.mock import patch

with patch('sys.stdout', new=StringIO()) as fakeOutput:
    print('hello world')
    self.assertEqual(fakeOutput.getvalue().strip(), 'hello world')

3
Adoro questo per Python 3, è pulito!
Sylhare

1
Questa era l'unica soluzione su questa pagina che ha funzionato per me! Grazie.
Justin Eyster,

24

In python 3.5 puoi usare contextlib.redirect_stdout()e StringIO(). Ecco la modifica al tuo codice

import contextlib
from io import StringIO
from foomodule import foo

def test_foo():
    temp_stdout = StringIO()
    with contextlib.redirect_stdout(temp_stdout):
        foo()
    output = temp_stdout.getvalue().strip()
    assert output == 'hello world!'

Bella risposta! Secondo la documentazione, questo è stato aggiunto in Python 3.4.
Hypercube

È 3.4 per redirect_stdout e 3.5 per redirect_stderr. forse è lì che è nata la confusione!
rbennell

redirect_stdout()e redirect_stderr()restituisce il loro argomento di input. Quindi, with contextlib.redirect_stdout(StringIO()) as temp_stdout:ti dà tutto in una riga. Testato con 3.7.1.
Adrian W

17

Sto solo imparando Python e mi sono trovato alle prese con un problema simile a quello sopra con i test unitari per i metodi con output. Il mio test unitario di passaggio per il modulo foo sopra ha finito per assomigliare a questo:

import sys
import unittest
from foo import foo
from StringIO import StringIO

class FooTest (unittest.TestCase):
    def setUp(self):
        self.held, sys.stdout = sys.stdout, StringIO()

    def test_foo(self):
        foo()
        self.assertEqual(sys.stdout.getvalue(),'hello world!\n')

5
Potresti voler fare un sys.stdout.getvalue().strip()confronto e non barare con \n:)
Silviu

Il modulo StringIO è deprecato. Invecefrom io import StringIO
Edwarric

10

La scrittura di test spesso ci mostra un modo migliore per scrivere il nostro codice. Simile alla risposta di Shane, vorrei suggerire un altro modo di vedere la cosa. Vuoi davvero affermare che il tuo programma ha emesso una certa stringa, o semplicemente che ha costruito una certa stringa per l'output? Questo diventa più facile da testare, poiché probabilmente possiamo presumere che l' printistruzione Python svolga correttamente il suo lavoro.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Allora il tuo test è molto semplice:

def test_foo_msg():
    assert 'hello world' == foo_msg()

Naturalmente, se hai davvero bisogno di testare l'output effettivo del tuo programma, sentiti libero di ignorarlo. :)


1
ma in questo caso foo non verrà testato ... forse è un problema
Pedro Valencia

5
Dal punto di vista di un purista del test, forse è un problema. Da un punto di vista pratico, se foo()non fa altro che chiamare l'istruzione print, probabilmente non è un problema.
Alison R.

5

Sulla base della risposta di Rob Kennedy, ho scritto una versione basata su classi del gestore di contesto per bufferizzare l'output.

L'utilizzo è come:

with OutputBuffer() as bf:
    print('hello world')
assert bf.out == 'hello world\n'

Ecco l'implementazione:

from io import StringIO
import sys


class OutputBuffer(object):

    def __init__(self):
        self.stdout = StringIO()
        self.stderr = StringIO()

    def __enter__(self):
        self.original_stdout, self.original_stderr = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = self.stdout, self.stderr
        return self

    def __exit__(self, exception_type, exception, traceback):
        sys.stdout, sys.stderr = self.original_stdout, self.original_stderr

    @property
    def out(self):
        return self.stdout.getvalue()

    @property
    def err(self):
        return self.stderr.getvalue()

2

Oppure considera l'utilizzo pytest, ha il supporto integrato per affermare stdout e stderr. Vedi i documenti

def test_myoutput(capsys): # or use "capfd" for fd-level
    print("hello")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    print("next")
    captured = capsys.readouterr()
    assert captured.out == "next\n"

Bene. Puoi includere un esempio minimo poiché i collegamenti possono scomparire e il contenuto può cambiare?
KobeJohn

2

Sia n611x007 che Noumenon hanno già suggerito l'uso unittest.mock, ma questa risposta adatta Acumenus per mostrare come puoi facilmente avvolgere i unittest.TestCasemetodi per interagire con un deriso stdout.

import io
import unittest
import unittest.mock

msg = "Hello World!"


# function we will be testing
def foo():
    print(msg, end="")


# create a decorator which wraps a TestCase method and pass it a mocked
# stdout object
mock_stdout = unittest.mock.patch('sys.stdout', new_callable=io.StringIO)


class MyTests(unittest.TestCase):

    @mock_stdout
    def test_foo(self, stdout):
        # run the function whose output we want to test
        foo()
        # get its output from the mocked stdout
        actual = stdout.getvalue()
        expected = msg
        self.assertEqual(actual, expected)

0

Basandomi su tutte le fantastiche risposte in questo thread, è così che l'ho risolto. Volevo mantenerlo il più disponibile possibile. Ho aumentato il meccanismo di unit test utilizzando setUp()per acquisire sys.stdoute sys.stderr, aggiunto nuove API di asserzione per controllare i valori catturati rispetto a un valore atteso e quindi ripristinare sys.stdoute sys.stderrsu tearDown(). I did this to keep a similar unit test API as the built-inunittest API while still being able to unit test values printed tosys.stdout orsys.stderr`.

import io
import sys
import unittest


class TestStdout(unittest.TestCase):

    # before each test, capture the sys.stdout and sys.stderr
    def setUp(self):
        self.test_out = io.StringIO()
        self.test_err = io.StringIO()
        self.original_output = sys.stdout
        self.original_err = sys.stderr
        sys.stdout = self.test_out
        sys.stderr = self.test_err

    # restore sys.stdout and sys.stderr after each test
    def tearDown(self):
        sys.stdout = self.original_output
        sys.stderr = self.original_err

    # assert that sys.stdout would be equal to expected value
    def assertStdoutEquals(self, value):
        self.assertEqual(self.test_out.getvalue().strip(), value)

    # assert that sys.stdout would not be equal to expected value
    def assertStdoutNotEquals(self, value):
        self.assertNotEqual(self.test_out.getvalue().strip(), value)

    # assert that sys.stderr would be equal to expected value
    def assertStderrEquals(self, value):
        self.assertEqual(self.test_err.getvalue().strip(), value)

    # assert that sys.stderr would not be equal to expected value
    def assertStderrNotEquals(self, value):
        self.assertNotEqual(self.test_err.getvalue().strip(), value)

    # example of unit test that can capture the printed output
    def test_print_good(self):
        print("------")

        # use assertStdoutEquals(value) to test if your
        # printed value matches your expected `value`
        self.assertStdoutEquals("------")

    # fails the test, expected different from actual!
    def test_print_bad(self):
        print("@=@=")
        self.assertStdoutEquals("@-@-")


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

Quando viene eseguito lo unit test, l'output è:

$ python3 -m unittest -v tests/print_test.py
test_print_bad (tests.print_test.TestStdout) ... FAIL
test_print_good (tests.print_test.TestStdout) ... ok

======================================================================
FAIL: test_print_bad (tests.print_test.TestStdout)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tests/print_test.py", line 51, in test_print_bad
    self.assertStdoutEquals("@-@-")
  File "/tests/print_test.py", line 24, in assertStdoutEquals
    self.assertEqual(self.test_out.getvalue().strip(), value)
AssertionError: '@=@=' != '@-@-'
- @=@=
+ @-@-


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.