Bagaimana cara menegaskan keluaran dengan nosetest / unittest di python?

114

Saya sedang menulis tes untuk fungsi seperti berikut:

def foo():
    print 'hello world!'

Jadi ketika saya ingin menguji fungsi ini kodenya akan seperti ini:

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

Tetapi jika saya menjalankan nosetests dengan parameter -s, pengujian akan macet. Bagaimana saya bisa menangkap output dengan modul unittest atau nose?

Pedro Valencia
sumber

Jawaban:

125

Saya menggunakan manajer konteks ini untuk menangkap keluaran. Ini pada akhirnya menggunakan teknik yang sama seperti beberapa jawaban lainnya dengan mengganti sementara sys.stdout. Saya lebih suka manajer konteks karena ini membungkus semua pembukuan menjadi satu fungsi, jadi saya tidak perlu menulis ulang kode coba-akhirnya, dan saya tidak perlu menulis fungsi penyiapan dan pembongkaran hanya untuk ini.

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

Gunakan seperti ini:

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

Selanjutnya, karena status keluaran asli dipulihkan setelah keluar dari withblok, kita dapat mengatur blok penangkapan kedua dalam fungsi yang sama seperti yang pertama, yang tidak mungkin menggunakan fungsi pengaturan dan pembongkaran, dan menjadi bertele-tele saat menulis coba-akhirnya memblokir secara manual. Kemampuan itu berguna saat tujuan pengujian adalah untuk membandingkan hasil dari dua fungsi relatif satu sama lain daripada dengan beberapa nilai yang telah dihitung sebelumnya.

Rob Kennedy
sumber
Ini telah bekerja sangat baik untuk saya di pep8radius . Namun baru-baru ini, saya telah menggunakan ini lagi dan mendapatkan kesalahan berikut saat mencetak TypeError: unicode argument expected, got 'str'(jenis yang diteruskan untuk dicetak (str / unicode) tidak relevan).
Andy Hayden
9
Hmmm bisa jadi di python 2 kita mau from io import BytesIO as StringIOdan di python 3 saja from io import StringIO. Sepertinya memperbaiki masalah dalam pengujian saya, saya pikir.
Andy Hayden
4
Ooop, untuk menyelesaikannya, mohon maaf atas begitu banyak pesan. Hanya untuk memperjelas bagi orang-orang yang menemukan ini: python3 gunakan io.StringIO, python 2 gunakan StringIO.StringIO! Terima kasih lagi!
Andy Hayden
Mengapa semua contoh di sini meminta strip()yang unicodedikembalikan dari StringIO.getvalue()?
Palimondo
1
Tidak, @Vedran. Ini bergantung pada penyatuan kembali nama yang dimilikinya sys. Dengan pernyataan import Anda, Anda membuat variabel lokal bernama stderryang menerima salinan nilai dalam sys.stderr. Perubahan satu tidak tercermin di yang lain.
Rob Kennedy
60

Jika Anda benar-benar ingin melakukan ini, Anda dapat menetapkan kembali sys.stdout selama pengujian.

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

Jika saya menulis kode ini, bagaimanapun, saya lebih suka memberikan outparameter opsional ke foofungsi.

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

Maka tesnya jauh lebih sederhana:

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

    out = StringIO()
    foo(out=out)
    output = out.getvalue().strip()
    assert output == 'hello world!'
Shane Hathaway
sumber
11
Catatan: Di bawah python 3.x, StringIOkelas sekarang harus diimpor dari iomodul. from io import StringIObekerja di python 2.6+.
Bryan P
2
Jika Anda menggunakan from io import StringIOpython 2, Anda mendapatkan TypeError: unicode argument expected, got 'str'saat mencetak.
matiasg
9
Catatan singkat : Dalam python 3.4, Anda dapat menggunakan pengelola konteks contextlib.redirect_stdout untuk melakukan ini dengan cara yang aman untuk pengecualian:with redirect_stdout(out):
Lucretiel
2
Anda tidak perlu melakukannya saved_stdout = sys.stdout, Anda selalu memiliki referensi ajaib untuk ini sys.__stdout__, misalnya, Anda hanya perlu sys.stdout = sys.__stdout__dalam pembersihan.
ThorSummoner
@ThorSummoner Terima kasih, ini hanya menyederhanakan beberapa tes saya ... untuk scuba yang saya lihat Anda telah membintangi .... dunia kecil!
Jonathon Reinhart
48

Sejak versi 2.7, Anda tidak perlu lagi menetapkan kembali sys.stdout, ini disediakan melalui bufferflag . Selain itu, ini adalah perilaku default nosetest.

Berikut ini contoh yang gagal dalam konteks non 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!')

Anda dapat mengatur buffer melalui unit2bendera baris perintah -b, --bufferatau dalam unittest.mainopsi. Kebalikannya dicapai melalui nosetestbendera --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)
FabienAndre
sumber
Perhatikan bahwa ini berinteraksi dengan --nocapture; khususnya, jika flag ini diset, mode buffer akan dinonaktifkan. Jadi, Anda memiliki opsi untuk dapat melihat keluaran di terminal, atau dapat menguji apakah keluarannya sesuai dengan yang diharapkan.
ijoseph
1
Apakah mungkin untuk mengaktifkan dan menonaktifkannya untuk setiap pengujian, karena hal ini membuat proses debug menjadi sangat sulit saat menggunakan sesuatu seperti ipdb.set_trace ()?
Lqueryvg
33

Banyak dari jawaban ini gagal untuk saya karena Anda tidak bisa from StringIO import StringIOmenggunakan Python 3. Berikut cuplikan kerja minimum berdasarkan komentar @ naxa dan 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')
Noumenon
sumber
3
Saya suka yang ini untuk Python 3, bersih!
Sylhare
1
Ini adalah satu-satunya solusi di halaman ini yang berhasil untuk saya! Terima kasih.
Justin Eyster
24

Di python 3.5 Anda dapat menggunakan contextlib.redirect_stdout()dan StringIO(). Berikut modifikasi pada kode Anda

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!'
Mudit Jain
sumber
Jawaban yang bagus! Menurut dokumentasi ini ditambahkan dengan Python 3.4.
Hypercube
Ini 3.4 untuk redirect_stdout dan 3.5 untuk redirect_stderr. mungkin di situlah kebingungan muncul!
rbennell
redirect_stdout()dan redirect_stderr()mengembalikan argumen masukan mereka. Jadi, with contextlib.redirect_stdout(StringIO()) as temp_stdout:berikan Anda semua dalam satu baris. Diuji dengan 3.7.1.
Adrian W
17

Saya baru saja belajar Python dan mendapati diri saya berjuang dengan masalah yang mirip dengan masalah di atas dengan pengujian unit untuk metode dengan keluaran. Uji unit kelulusan saya untuk modul foo di atas akhirnya terlihat seperti ini:

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')
sean_robbins
sumber
5
Anda mungkin ingin melakukan sys.stdout.getvalue().strip()dan tidak menipu dibandingkan dengan \n:)
Silviu
Modul StringIO tidak digunakan lagi. Sebaliknyafrom io import StringIO
Edwarric
10

Tes menulis sering kali menunjukkan kepada kita cara yang lebih baik untuk menulis kode kita. Mirip dengan jawaban Shane, saya ingin menyarankan cara lain untuk melihat ini. Apakah Anda benar-benar ingin menegaskan bahwa program Anda mengeluarkan string tertentu, atau hanya ia membangun string tertentu untuk keluaran? Ini menjadi lebih mudah untuk diuji, karena kita mungkin dapat berasumsi bahwa printpernyataan Python melakukan tugasnya dengan benar.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Maka tes Anda sangat sederhana:

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

Tentu saja, jika Anda benar-benar perlu menguji keluaran aktual program Anda, abaikan saja. :)

Alison R.
sumber
1
tetapi dalam kasus ini foo tidak akan diuji ... mungkin itu masalah
Pedro Valencia
5
Dari perspektif penguji purist, mungkin itu masalah. Dari sudut pandang praktis, jika foo()tidak melakukan apapun kecuali memanggil pernyataan cetak, itu mungkin tidak menjadi masalah.
Alison R.
5

Berdasarkan jawaban Rob Kennedy, saya menulis versi manajer konteks berbasis kelas untuk menyangga keluaran.

Penggunaannya seperti:

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

Berikut implementasinya:

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()
Hugo Mota
sumber
2

Atau pertimbangkan untuk menggunakan pytest, ia memiliki dukungan built-in untuk menegaskan stdout dan stderr. Lihat dokumen

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"
Michel Samia
sumber
Bagus. Bisakah Anda menyertakan contoh minimal karena tautan bisa hilang dan konten bisa berubah?
KobeJohn
2

Baik n611x007 dan Noumenon sudah menyarankan penggunaan unittest.mock, tetapi jawaban ini menyesuaikan Acumenus untuk menunjukkan bagaimana Anda dapat dengan mudah membungkus unittest.TestCasemetode untuk berinteraksi dengan tiruan 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)
rovyko
sumber
0

Membangun semua jawaban luar biasa di utas ini, inilah cara saya menyelesaikannya. Saya ingin menyimpannya sebagai stok. Aku ditambah mekanisme uji unit menggunakan setUp()untuk menangkap sys.stdoutdan sys.stderr, menambahkan API menegaskan baru untuk memeriksa nilai-nilai yang diambil terhadap nilai yang diharapkan dan kemudian mengembalikan sys.stdoutdan sys.stderrpada 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()

Saat unit test dijalankan, hasilnya adalah:

$ 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)
sorens
sumber