งูขดตัวต้นฉบับกำลังเบนกรวยเสียงจากลำโพงเก่าเข้าสมุดบันทึกบนโต๊ะสว่าง

Python redirect_stdout: จับ output ของโค้ดเก่าไว้ทดสอบ

พี่โชว์Engineering

บาง library เก่ายัง print() ผลลัพธ์แทนการคืนค่า ถ้าจะทดสอบหรือฝังใน notebook การแก้ทุก call site อาจเสี่ยงเกินไป contextlib.redirect_stdout ช่วยเบน stdout ชั่วคราวเข้า file-like object เหมือนต่อท่อจากลำโพงเข้าเครื่องบันทึกครับ

import io
import unittest
from contextlib import redirect_stdout


def legacy_report(items):
    print(f"count={len(items)}")
    for item in items:
        print(f"- {item}")


def capture_report(items):
    stream = io.StringIO()
    with redirect_stdout(stream):
        legacy_report(items)
    return stream.getvalue()


class RedirectStdoutTests(unittest.TestCase):
    def test_captures_complete_report(self):
        self.assertEqual(capture_report(["mint", "amber"]), "count=2\n- mint\n- amber\n")

    def test_empty_report(self):
        self.assertEqual(capture_report([]), "count=0\n")


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

ใช้ตรงไหนและไม่ควรใช้ตรงไหน

เหมาะกับ test, help text และ adapter รอบ legacy code แต่ไม่เหมาะเป็นระบบ logging เพราะมันเปลี่ยน sys.stdout แบบ global ระหว่าง context ทำให้ thread อื่นอาจเขียนผิดปลายทาง และจับ output จาก subprocess ที่เขียน file descriptor ตรง ๆ ไม่ได้ สำหรับ production ให้ refactor ฟังก์ชันคืนข้อมูลหรือรับ writer/logger ผ่าน dependency แทน

Recap

  • จำกัด redirect ให้อยู่ใน with ที่สั้นที่สุด
  • ทดสอบ newline และ empty input แบบ exact
  • อย่าใช้แทน structured logging
  • หลีกเลี่ยงในโค้ดหลาย thread