Como afirmar a saída com nosetest / unittest em python?


114

Estou escrevendo testes para uma função como a próxima:

def foo():
    print 'hello world!'

Então, quando eu quiser testar essa função, o código será assim:

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!'

Mas se eu executar nosetests com o parâmetro -s, o teste falha. Como posso capturar a saída com unittest ou módulo nariz?


Respostas:


124

Eu uso este gerenciador de contexto para capturar a saída. Em última análise, usa a mesma técnica que algumas das outras respostas, substituindo temporariamente sys.stdout. Eu prefiro o gerenciador de contexto porque ele envolve toda a contabilidade em uma única função, então eu não tenho que reescrever nenhum código try-finally e não tenho que escrever funções de configuração e desmontagem apenas para isso.

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

Use-o assim:

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!')

Além disso, uma vez que o estado de saída original é restaurado ao sair do withbloco, podemos configurar um segundo bloco de captura na mesma função do primeiro, o que não é possível usando as funções de configuração e desmontagem, e fica prolixo ao escrever try-finalmente blocos manualmente. Essa capacidade foi útil quando o objetivo de um teste era comparar os resultados de duas funções entre si, em vez de algum valor pré-calculado.


Isso funcionou muito bem para mim em Pep8radius . Recentemente, porém, usei isso novamente e recebo o seguinte erro ao imprimir TypeError: unicode argument expected, got 'str'(o tipo passado para impressão (str / unicode) é irrelevante).
Andy Hayden,

9
Hmmm pode ser que no python 2 queiramos from io import BytesIO as StringIOe no python 3 apenas from io import StringIO. Pareceu corrigir o problema em meus testes, eu acho.
Andy Hayden,

4
Ooop, só para terminar, desculpas por tantas mensagens. Apenas para esclarecer para quem está achando isso: python3 use io.StringIO, python 2 use StringIO.StringIO! Obrigado novamente!
Andy Hayden,

Por que todos os exemplos aqui estão chamando strip()o unicoderetornado de StringIO.getvalue()?
Palimondo

1
Não, @Vedran. Isso depende da religação do nome ao qual pertence sys. Com sua instrução de importação, você está criando uma variável local chamada stderrque recebeu uma cópia do valor em sys.stderr. Mudanças em um não se refletem no outro.
Rob Kennedy

60

Se você realmente deseja fazer isso, pode reatribuir sys.stdout para a duração do teste.

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 eu estivesse escrevendo este código, no entanto, preferiria passar um outparâmetro opcional para a foofunção.

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

Então, o teste é muito mais simples:

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: No python 3.x, a StringIOclasse agora deve ser importada do iomódulo. from io import StringIOfunciona em python 2.6+.
Bryan P

2
Se você usar from io import StringIOem python 2, obterá um TypeError: unicode argument expected, got 'str'ao imprimir.
matiasg

9
Observação rápida: no python 3.4, você pode usar o gerenciador de contexto contextlib.redirect_stdout para fazer isso de uma maneira que seja segura para exceções:with redirect_stdout(out):
Lucretiel

2
Você não precisa fazer isso saved_stdout = sys.stdout, você sempre tem uma referência mágica para fazer isso sys.__stdout__, por exemplo, você só precisa sys.stdout = sys.__stdout__na sua limpeza.
ThorSummoner

@ThorSummoner Obrigado, isso apenas simplificou alguns dos meus testes ... para mergulho que vejo que você estrelou .... pequeno mundo!
Jonathon Reinhart

48

Desde a versão 2.7, você não precisa mais reatribuir sys.stdout, isso é fornecido por meio de bufferflag . Além disso, é o comportamento padrão do nosetest.

Aqui está um exemplo de falha em contexto sem buffer:

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!')

Você pode definir tampão através unit2flag de linha de comando -b, --bufferou em unittest.mainopções. O oposto é conseguido por meio da nosetestbandeira --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)

Observe que isso interage com --nocapture; em particular, se este sinalizador for definido, o modo buffer será desabilitado. Portanto, você tem a opção de ver a saída no terminal ou de testar se a saída é a esperada.
ijoseph

1
É possível ligar e desligar isso para cada teste, porque isso torna a depuração muito difícil ao usar algo como ipdb.set_trace ()?
Lqueryvg

33

Muitas dessas respostas falharam para mim porque você não pode from StringIO import StringIOno Python 3. Aqui está um trecho de trabalho mínimo baseado no comentário de @naxa e no 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
Eu amo este para Python 3, é limpo!
Sylhare

1
Esta foi a única solução nesta página que funcionou para mim! Obrigado.
Justin Eyster

24

No python 3.5 você pode usar contextlib.redirect_stdout()e StringIO(). Aqui está a modificação em seu código

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!'

Ótima resposta! De acordo com a documentação, isso foi adicionado no Python 3.4.
Hypercube

É 3,4 para redirect_stdout e 3,5 para redirect_stderr. talvez seja aí que surgiu a confusão!
rbennell

redirect_stdout()e redirect_stderr()retornar seu argumento de entrada. Então, with contextlib.redirect_stdout(StringIO()) as temp_stdout:dá a você tudo em uma linha. Testado com 3.7.1.
Adrian W

17

Estou apenas aprendendo Python e me deparei com um problema semelhante ao acima com testes de unidade para métodos com saída. Minha aprovação no teste de unidade para o módulo foo acima acabou ficando assim:

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
Você pode querer fazer um sys.stdout.getvalue().strip()e não trapacear comparando com \n:)
Silviu

O módulo StringIO está obsoleto. Em vez dissofrom io import StringIO
Edwarric

10

Escrever testes geralmente nos mostra uma maneira melhor de escrever nosso código. Semelhante à resposta de Shane, gostaria de sugerir outra maneira de ver isso. Você realmente deseja afirmar que seu programa produziu uma determinada string ou apenas que construiu uma determinada string para a saída? Isso se torna mais fácil de testar, já que provavelmente podemos supor que a printinstrução Python faz seu trabalho corretamente.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Então, seu teste é muito simples:

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

Obviamente, se você realmente precisa testar a saída real do seu programa, fique à vontade para desconsiderar. :)


1
mas neste caso foo não será testado ... talvez seja um problema
Pedro Valencia

5
Do ponto de vista de um purista de testes, talvez seja um problema. Do ponto de vista prático, se foo()não fizer nada além de chamar a instrução print, provavelmente não é um problema.
Alison R.

5

Com base na resposta de Rob Kennedy, escrevi uma versão baseada em classe do gerenciador de contexto para armazenar a saída em buffer.

O uso é como:

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

Esta é a implementação:

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

Ou considere usar pytest, ele tem suporte integrado para declarar stdout e stderr. Veja a documentação

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"

Um bom. Você pode incluir um exemplo mínimo, já que os links podem desaparecer e o conteúdo pode mudar?
KobeJohn

2

Ambos n611x007 e Noumenon já sugeriu o uso unittest.mock, mas esta resposta se adapta Acumenos de mostrar como você pode facilmente envolver unittest.TestCasemétodos para interagir com um zombou 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

Com base em todas as respostas incríveis neste tópico, foi assim que resolvi. Eu queria mantê-lo o mais estoque possível. I aumentou o mecanismo de teste de unidade usando setUp()a captura sys.stdoute sys.stderr, adicionou novas APIs do assert para verificar os valores capturados contra um valor esperado e, em seguida, restaurar sys.stdoute sys.stderrsobre 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 o teste de unidade é executado, a saída é:

$ 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)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.