質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.47%

  • Python

    12268questions

    Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

  • Python 3.x

    10256questions

    Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

「1つのファイル」へのテストはどんなモジュール、方法を使えばよいでしょうか

解決済

回答 2

投稿

  • 評価
  • クリップ 1
  • VIEW 1,902

uni8inu

score 117

質問

Pythonではunittestモジュールを使ったユニットテストが一般的なテスト方法だと思います。調査した所、関数、あるいはクラスへのテストはunittestモジュールでできることはわかりました。
ただ、unittestを使って標準入出力を含む「1つの.pyファイルへのテスト」ができるのかが分かりませんでした。
このような場合は、どのようにテストすれば良いのか(どんなテストモジュールならいけるか)ご存知でしたらご教示願えますでしょうか?

具体的な例

下記のようなname.pyファイルがあり、標準入力値"wanko"で標準出力"wanko wanko"が期待されているという状況があった場合、どんなテストモジュールを使ってテストをやればよいか?

#name.pyのコード
name = input()
print(name,name)

質問の背景

Pythonビギナーです。
現在paizaでPyhonのコードのオンラインジャッジを行っております。
オンラインジャッジのシステムでは標準入力を受け取り、標準出力へ正しい結果が出ているかを判定しております。提出コードへ変更を加えずにテストを行いたいと考えており、この度質問いたしました。

試したこと

自作でsubprocessとPepoenを利用しstdin,stdoutを操作して、
ファイルに対してテストができるコードを書きましたが、なんというか車輪の再開発感がもりもりしています。
つらいです。

#ファイルに対してテストを行うモジュール
import subprocess
from subprocess import Popen,PIPE

# -------ここから変更--------
#テストを行うファイル名
test_filename = "name.py"

#""" """の間に標準入力テストデータを貼り付ける
input_d ="""
wanko
"""
#""" """の間に期待する標準出力結果を貼り付ける
expect_d = """
wanko wanko
"""
# -------ここまで--------
run_cmd = "python " + test_filename

input_d = input_d[1:] # スライスで行頭削除 : 改行単位でinput()が実行される
expect_d = expect_d[1:] # スライスで行頭削除 : stdoutは行末に改行を含む

p = Popen(run_cmd,
          stdin=PIPE,stdout=PIPE,stderr=subprocess.PIPE,
          universal_newlines=True)

p.stdin.write(input_d) # stdin実行
stdout = p.communicate()[0] # stdout取得

#結果の表示
print("[{} stdout]\n{}".format(test_filename,stdout))
print("[expect]\n{}".format(expect_d))

if(expect_d != stdout):
    print("***TEST NG***")
else:
    print("!!!TEST OK!!!")
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

+1

車輪の再開発感が~の下りを最初に読んだときに思い浮かんだのは、シェルで

diff <(python foo.py < IN.txt) OUT.txt

でした。

unittest を使うなら、入出力とロジック部分を分離して…等と書こうとしてたのですが、
やりたいことは、関数のロジックの単体テストではなく、入出力を用いた受入テストの方なのでしょうか。

  • Python内で標準入力の mock は sys.stdin へのストリームの代入で可能です。
  • 同様に標準出力のキャプチャも sys.stdout 差し替えで可能ですが、
    標準出力のキャプチャ&テストは、小規模なものであれば doctest を用いると楽です。
# File: my_code.py
name = input()
print(name, name)
#!/usr/bin/env python

# File: test_my_code.py
# 実行方法: python -m doctest -v test_my_code.py

import sys
from io import StringIO
from runpy import run_module

def test_my_code(stdin):
    """
    >>> test_my_code("wanko\\n")
    wanko wanko

    >>> test_my_code("nyaa\\n")
    nyaa nyaa

    """
    sys.stdin = StringIO(stdin)
    run_module("my_code")

規模が大きくなってくると、doctest では記述しにくいこともありますが
コードの実行が完了するまで待ってから、結果を比較するテスト方法では、
メモリ的にも時間的にも無駄が多くなるので、
テスト対象のコードをイテレータを返すように変更する等、
テストしやすいように元コードの設計を変える工夫が必要になってきます。


ちなみに、再利用可能なライブラリではないのですが、
Python自身が使ってるテストコードで test.support 以下に 
サブプロセスを用いた方法や、標準入出力のキャプチャ等の実装があり、
同じ様なことをやっているので、参考になるかと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/04 16:14

    非常に参考になりました!
    確かに簡易的にはシェルで結果取ってDiffすればよい・・・盲点でした。
    IDEに頼りすぎていて、シェルを使うという脳が死んでおりました。
    用語がわからず説明がうまく出来ませんでしたが、ご指摘の通り、知りたかったのは「入出力を用いた受入テスト」はどうやるのか?という事だったようです。
    頂いたアドバイスを参考に、最適な方法を選択したいと思います。ありがとうございました。

    キャンセル

checkベストアンサー

0

「1つの.pyファイルへのテスト」をテストも実装も含めて1つのファイル・・・と解釈しました。そうでなくとも、テスト対象をimportすれば同じことができますので、それで説明します。

unittest.mockを使います。モックとはケータイショップに置いてある中身の入ってない、あの模型のようなものです。これを使うとテストしたい対象が依存している副作用をテスト用のものに置き換えられます。下層のオブジェクトの副作用を含んだテストではなく、その関数の副作用だけを検証できます。Incremental Testと呼ばれます。

はい、うんちくはいいですね。こんな風に使います。

import unittest
import unittest.mock as mock
from contextlib import redirect_stdout
import io

def main():
    name = input()
    print(name, name)

class TestMain(unittest.TestCase):
    @mock.patch('__main__.input', return_value='hoge')
    def test_main0(self, *args, **kwarg):
        f = io.StringIO()
        with redirect_stdout(f):
            main()
        self.assertEqual(f.getvalue(), 'hoge hoge\n')

if __name__ == '__main__':
    import sys
    if len(sys.argv) == 2 and sys.argv[1] == 'test':
        unittest.main(argv=[sys.argv[0]])
    else:
        main()

この例ではinput()'hoge'を返すだけの関数に置き換えられています。return_valueだけでなく、side_effectで関数を指定することもできます。詳しくはドキュメントをご覧ください。

 追記

なるほどimport時に実行されるコードの副作用ですか。一応できなくはないみたいですが、なんかちょっと危なっかしい気もします。そういうことならsubprocessを使った方法もあながち悪くないかもしれませんね。

#wanko.py
name = input()
print(name,name)
import sys
import io
import unittest
import unittest.mock as mock

class TestWanko(unittest.TestCase):
    def setUp(self):
        self.orig_stdout = sys.stdout
        sys.stdout = io.StringIO()

    def tearDown(self):
        sys.stdout = self.orig_stdout
        if 'wanko' in sys.modules:
            del sys.modules['wanko']

    @mock.patch('builtins.input', return_value='hoge')
    def test0(self, *args, **kwarg):
        import wanko
        self.assertEqual(sys.stdout.getvalue(), 'hoge hoge\n')

    @mock.patch('builtins.input', return_value='wan')
    def test1(self, *args, **kwarg):
        import wanko
        self.assertEqual(sys.stdout.getvalue(), 'wan wan\n')

unittest.main()

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/04 00:53

    ご回答ありがとうございます。mockを知らなかったので大変参考になりました。
    "テスト対象をimportすれば同じことができます"の部分ですが、確かに参考コードのmain()部分をモジュール化した所うまくいきました。

    ただ、元々やりたかったことが「テスト対象のモジュールが素の実行文しかないもの」に対するテストです。質問の内容の書き方が悪く、申し訳ないです。以下のようなモジュールへのテストをしたいのです。
    ```python
    #my_code.py : 素の実行文しかないモジュール
    name = input
    print(name,name)
    ```
    通常の、「モジュール内の関数などへのテスト」はテストケースにimportした時点でモジュールがインスタンス化されて、モックのデコレーターが効くと思っております。
    一方、「素の実行文を持つモジュール」はimport時点で即座に処理が動き出し、mock化するチャンスが無いように思われます。試しに改造した下記のコードではうまく行きませんでした。
    ```python
    #教えていただいたサンプルコードをmy_code.pyを読み出すように改造
    #略
    class TestMain(unittest.TestCase):
    @mock.patch('my_code.input', return_value='hoge')
    def test_main0(self, *args, **kwarg):
    f = io.StringIO()
    with redirect_stdout(f):
    import my_code #モジュール実行後にインスタンス化されるため、mockが効かない?
    self.assertEqual(f.getvalue(), 'hoge hoge\n') #break point
    #略
    ```
    self.assertEqualにブレークポイントを貼ると、モジュールインスタンスはきちんとMagicMocを持っているようです。しかし、my_code.py内の処理はすでに実行済み・・・。
    input()があとからmoc化されても時すでに遅し!と言った感じになっておりました。
    素の実行文を持つモジュールに対しては、mocで対応できる範囲外、という感じでしょうか?

    キャンセル

  • 2016/11/04 10:02

    コメントだとマークダウンが効かないので回答に追記しました。
    inputのような組み込み関数は builtins.input でモック化できるようです。

    キャンセル

  • 2016/11/04 15:45

    builtins.input!これです。ありがとうございます。
    追記いただいたコードで、知りたかったテストの動作になりました。

    キャンセル

  • 2016/11/04 16:46

    teamiklさん方法も参考にしてください。
    unittest内でもimportでなくrunpyを使ったほうがよさそうです。
    私はrunpy知りませんでした。これを使うとdel sys.modules['wanko']とかは要らなくなりますね。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.47%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る

  • Python

    12268questions

    Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

  • Python 3.x

    10256questions

    Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。