0

I want to test a Python file but the file doesn't contain any functions and hence returns nothing.

example.py

x = input()
if int(x):
    print(x)

I don't want to make a function like this:

def check_x():
    x = input()
    if int(x):
        print(x) or return x

or

def check_x(x):
    if int(x):
        print(x) or return x

How to test a file containing an input call, or multiple input and print statements having no functions that return a value?

I use pytest for testing.

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
shraysalvi
  • 303
  • 1
  • 10
  • Kurt Cobain would say... *Smells Like Teen's Homework*. Why don't you want to use a function? – FLAK-ZOSO Aug 14 '22 at 19:58
  • @FLAK-ZOSO I am trying to create a website which test codes of user but I dont want to bound them to create a function and do stuf in that with returning something. So user can directly start without a function. – shraysalvi Aug 15 '22 at 20:58

1 Answers1

2

If you are using pytest, you can first monkeypatch the user input and the print-ed output, then import the example module (or whichever module you need to test), then check that the print-ed output matches the expected.

Given the example module (example.py) and a test file all in a directory named files:

$ tree files
files
├── example.py
└── test_example.py

The test function could look like this:

import builtins
import importlib
import io
import sys

import pytest
from pytest import MonkeyPatch

def test_example_123(monkeypatch: MonkeyPatch):
    mocked_input = lambda prompt="": "123"
    mocked_stdout = io.StringIO()

    with monkeypatch.context() as m:
        m.setattr(builtins, "input", mocked_input)
        m.setattr(sys, "stdout", mocked_stdout)

        sys.modules.pop("example", None)
        importlib.import_module(name="example", package="files")

    assert mocked_stdout.getvalue().strip() == "123"
$ python3.9 -m pytest --no-header -vv files/test_example.py
...
collected 1 item                                                                                                                                    

files/test_example.py::test_example_123 PASSED

The monkeypatch fixture can replace the attributes of objects and modules, to something that doesn't depend on any external inputs or on the environment.

In this case, the test patches 2 things:

  • The builtins.input function, such that it gets the input string from the test instead of stopping to get user input. The mocked user input is saved in mocked_input. If the module calls input multiple times, then you can change this from a lambda function to a regular function, that returns different strings based on how many times it was called or base it on the prompt.

    # example.py
    x = input("x")
    y = input("y")
    z = input("z")
    if int(x):
        print(x)
    
    # Hacky way of relying on mutable default arguments
    # to keep track of how many times mocked_input was called
    def mocked_input(prompt="", return_vals=["3333", "2222", "1111"]):
        return return_vals.pop(-1)
    
    # Return a fake value based on the prompt
    def mocked_input(prompt=""):
        return {"x": "1111", "y": "2222", "z": "3333"}[prompt]
    
  • The sys.stdout function, which is the default location where the builtins.print function will print out objects passed to it. The print outs are captured and stored in mocked_stdout, which is a io.StringIO instance compatible with print. As explained in the docs, multiple print's would result in still 1 string, but separated by \n, ex. 'print1\nprint2\nprint3'. You can just split and use as a list.

The last piece of the puzzle is using importlib to import that example module (what you call the "single python file" example.py) during runtime and only when the test is actually run.

The code

importlib.import_module(name="example", package="files")

is similar to doing

from files import example

The problem with your "single python file" is that, since none of the code is wrapped in functions, all the code will be immediately run the moment that module is imported. Furthermore, Python caches imported modules, so the patched input and print would only take effect when the module is first imported. This is a problem when you need to re-run the module multiple times for multiple tests.

As a workaround, you can pop-off the cached "examples" module from sys.modules before importing

sys.modules.pop("example", None)

When the module is successfully imported, mocked_stdout should now have whatever is supposed to be print-ed out to sys.stdout. You can then just do a simple assertion check.

To test multiple input and output combinations, use pytest.mark.parametrize to pass-in different test_input and expected_output, replacing the hardcoded "123" from the previous code.

@pytest.mark.parametrize(
    "test_input, expected_output",
    [
        ("456", "456"),
        ("-999", "-999"),
        ("0", ""),  # Expect int("0") to be 0, so it is False-y
    ],
)
def test_example(monkeypatch: MonkeyPatch, test_input: str, expected_output: str):
    mocked_input = lambda prompt="": test_input
    mocked_stdout = io.StringIO()

    with monkeypatch.context() as m:
        m.setattr(builtins, "input", mocked_input)
        m.setattr(sys, "stdout", mocked_stdout)

        sys.modules.pop("example", None)
        importlib.import_module(name="example", package="files")

    assert mocked_stdout.getvalue().strip() == expected_output
$ python3.9 -m pytest --no-header -vv files/test_example.py
...
collected 4 items                                                                                                                                   

files/test_example.py::test_example_123 PASSED
files/test_example.py::test_example[456-456] PASSED
files/test_example.py::test_example[-999--999] PASSED
files/test_example.py::test_example[0-] PASSED

Lastly, for cases like "4.56" where you expect an ValueError, it's more useful to just test that an Exception was raised rather than checking the print-ed output.

@pytest.mark.parametrize(
    "test_input",
    [
        "9.99",
        "not an int",
    ],
)
def test_example_errors(monkeypatch: MonkeyPatch, test_input: str):
    mocked_input = lambda prompt="": test_input

    with monkeypatch.context() as m:
        m.setattr(builtins, "input", mocked_input)

        sys.modules.pop("example", None)
        with pytest.raises(ValueError) as exc:
            importlib.import_module(name="example", package="files")

    assert str(exc.value) == f"invalid literal for int() with base 10: '{test_input}'"
$ python3.9 -m pytest --no-header -vv files/test_example.py
...
collected 6 items                                                                                                                                   

files/test_example.py::test_example_123 PASSED
files/test_example.py::test_example[123-123] PASSED
files/test_example.py::test_example[-999--999] PASSED
files/test_example.py::test_example[0-] PASSED
files/test_example.py::test_example_errors[9.99] PASSED
files/test_example.py::test_example_errors[not an int] PASSED
Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
  • How do I get JSON response per each test case. Ex `{"test_case_1": {"input": input, "your_output": output, "expected_output": expected_output, "result": pass or fail}, "test_case_2": .........so on}` – shraysalvi Aug 20 '22 at 14:35
  • @shraysalvi That's a completely new problem, so please [ask a new question](https://stackoverflow.com/questions/ask). See [What is the the best way to ask follow up questions?](https://meta.stackoverflow.com/q/266767/2745495). You can try checking https://pypi.org/project/pytest-json-report/. – Gino Mempin Aug 20 '22 at 15:05
  • I already tried it but it gives me the general information about the test I also tried to change `jsonmetadata` but maybe I am somewhere wrong. – shraysalvi Aug 20 '22 at 17:05
  • Ok I got it `pytest -s --json-report --json-report-file=report.json --log-cli-level=INFO` is used to get the json result with collectors. also, can add custom `jsonmetadeta`. – shraysalvi Aug 20 '22 at 18:50
  • How do I test this with my `check.py` file, As you give me the solution with pre-defined `expected_output` as a string now how do I test the user's code with my `check.py` file using the same inputs that we provide in test file. For example `check.py` contain solution code now I want to compare solution code with users code but with the same input for both files. (mock_input for both files solution and user's code). suppose `check.py` is also present in our tree files in the files directory. – shraysalvi Aug 27 '22 at 14:48