Calling a program with command line parameters with Pytest - python-3.x

This is my first time using Pytest, I have a program that is called with command line parameters, as in :
$ myprog -i value_a -o value_b
I am not sure how to use Pytest to test the output of this program. Given values of value_a and value_b, I expect a certain output that I want to test.
The Pytest examples that I see all refer to testing functions, for instance if there is a function such as:
import pytest
def add_nums(x,y):
return x + y
def test_add_nums():
ret = add_nums(2,2)
assert ret == 4
But I am not sure how to call my program using Pytest and not just test individual functions? Do I need to use os.system() and then call my program that way?
In my program I am using argparse module.

The solution is based on monkeypatch fixture. In below example myprog reads number from the file myprog_input.txt adds 2 to it and stores result in myprog_output.txt
Program under test
cat myprog.py
#!/usr/bin/python3.9
import argparse
import hashlib
def main():
parser = argparse.ArgumentParser(description='myprog')
parser.add_argument('-i')
parser.add_argument('-o')
args = parser.parse_args()
with open(args.i) as f:
input_data=int(f.read())
output_data=input_data+2
f.close()
with open(args.o,"w") as fo:
fo.write(str(output_data) + '\n')
fo.close()
with open(args.o) as fot:
bytes = fot.read().encode() # read entire file as bytes
fot.close()
readable_hash = hashlib.sha256(bytes).hexdigest();
return readable_hash
if __name__ == '__main__':
print(main())
Test
cat test_myprog.py
#!/usr/bin/python3.9
import sys
import myprog
def test_myprog(monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['myprog', '-i', 'myprog_input.txt', '-o', 'myprog_output.txt'])
assert myprog.main() == 'f0b5c2c2211c8d67ed15e75e656c7862d086e9245420892a7de62cd9ec582a06'
Input file
cat myprog_input.txt
3
Running the program
myprog.py -i myprog_input.txt -o myprog_output.txt
f0b5c2c2211c8d67ed15e75e656c7862d086e9245420892a7de62cd9ec582a06
Testing the program
pytest test_myprog.py
============================================= test session starts =============================================
platform linux -- Python 3.9.5, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/<username>/py
plugins: hypothesis-6.23.1
collected 1 item
test_myprog.py . [100%]
============================================== 1 passed in 0.04s ==============================================

Related

How to Unittest sys.argv with multiple values

Let's say I have a Python script file that uses sys.argv variables. And I want to test the sys.argv with different values in multiple tests. The issue is that I have to patch and import it multiple times but i think it holds the sys.args values only from the first import so both TestCases print the same values ['test1', 'Test1']. Is my approach wrong?
example.py
import sys
ex1 = sys.argv[0]
ex2 = sys.argv[1]
print(ex1)
print(ex2)
test_example.py
import unittest
import mock
import sys
class TestExample(unittest.TestCase):
#mock.patch.object(sys, 'argv', ['test1', 'Test1'])
def test_example1(self):
import example
#mock.patch.object(sys, 'argv', ['test2', 'Test2'])
def test_example2(self):
import example
The problem is that Python will not (unless you work hard at it) reimport the same file more than once.
What you should do is have the import only define stuff, and define a main() function in your module that gets called when it's time to run stuff. You call this main() function directly in the script when the script is called by itself (see the last two lines of example.py below), but then you can also call that function by name during unit testing.
For what it's worth, this is best practice in Python: your scripts and module should mostly define functions, classes and whatnot on import, but not run much. Then you call your main() function in a __name__ guard. See What does if __name__ == "__main__": do? for more details about that.
Here's your code modified to work, testing your module with two different sets of sys.argv values:
example.py:
import sys
def main():
ex1 = sys.argv[0]
ex2 = sys.argv[1]
print(ex1)
print(ex2)
if __name__ == "__main__":
main()
test-example.py:
import unittest
import mock
import sys
import example
class TestExample(unittest.TestCase):
#mock.patch.object(sys, 'argv', ['test1', 'Test1'])
def test_example1(self):
example.main()
#mock.patch.object(sys, 'argv', ['test2', 'Test2'])
def test_example2(self):
example.main()
Results from python -m unittest:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
test1
Test1
test2
Test2

Python subprocess for docker-compose

I have an interesting set of requirements that I am trying to conduct using the Python subprocess module and docker-compose. This whole setup is possible in one docker-compose but due to requirement this is what I would like to setup:
call the docker-compose using python subprocess to activate the
test-servers
print all the std-out of above docker-compose running.
as soon as the test-server up and running via docker-compose; call the testing scripts for that server.
This is my docker-compose.py looks like:
import subprocess
from subprocess import PIPE
import os
from datetime import datetime
class MyLog:
def my_log(self, message):
date_now = datetime.today().strftime('%d-%m-%Y %H:%M:%S')
print("{0} || {1}".format(date_now, message))
class DockercomposeRun:
log = MyLog()
def __init__(self):
dir_name, _ = os.path.split(os.path.abspath(__file__))
self.dirname = dir_name
def run_docker_compose(self, filename):
command_name = ["docker-compose", "-f", self.dirname + filename, "up"]
popen = subprocess.Popen(command_name, stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
return popen
now in my test.py
as soon as my stdout is blank I would like to break the loop of printing and run the rest of the test in test.py.
docker_compose_run = DockercomposeRun()
rc = docker_compose_run.run_docker_compose('/docker-compose.yml.sas-viya-1')
for line in iter(rc.stdout.readline, ''):
print(line, end='')
if line == '':
break
popen.stdout.close()
# start here actual test cases
.......
But for me the loop is never broken even though the stdout of docker-compose goes blank after the server is up and running. And, the test cases are never executed.
Is it the right approach or how I can achieve this?
I think the issue here is because you are not running docker-compose in detached mode and its blocking the application run. Can you try adding "-d" to command_name?

unit test in python for function that has no return value

I have a function that should pick only top 10 requests from list of requests:
def prioritize_top_10_requests(list_of_requests):
if not list_of_requests:
system.exit(1)
else:
for i in list_of_requests[0:10]:
print i
I want to write a unit test for this function just to check if 'i' is printed 10 times only though we have more list of requests.
I am writing python unit test cases for the first time.
I don't have python3 set up. The below code works with Python 3.
What it does is the following.
Created a UnitTest TestCase with one test, "test_priority".
In the test, redirect the stdout to a string.
Then execute the function.
Collect the print results from the string.
Remove None and empty strings and find the length.
Check if it is equal to 10.
import unittest
from io import StringIO
import sys
def prioritize_top_10_requests(list_of_requests):
for i in list_of_requests[0:10]:
print(i)
class TestMyCode(unittest.TestCase):
def test_priority(self):
sys.stdout = result = StringIO()
prioritize_top_10_requests(range(100))
sys.stdout = sys.__stdout__
printed_lines = result.getvalue()
print_count = len(printed_lines.strip().split('\n'))
self.assertEqual(print_count, 10)
if __name__ == '__main__':
unittest.main()

How to capture the user command, when a python script is called?

Let's say I have a python script myscript.py, which takes multiple named arguments. The user calls the script as python myscript.py --arg1 value1 --arg2 value2. How to capture this whole command (i.e. python myscript.py --arg1 value1 --arg2 value2 ) and save it to a text file "command.selfie" using the same script that the user is calling?
sys.argv is a list with all the arguments.
This will help:
import sys
command = " ".join(sys.argv)
# do whatever you want to do with command: str
You can do something like this:
import argparse
import sys
def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument("--arg1", default=False, help="Explain arg1")
parser.add_argument("--arg2", type=str, default="hello", help="Explain arg2")
# add all arguments you need
args = parser.parse_args(argv[1:])
params = {"arg1": args.arg1,
"arg2": args.arg2, }
print(params["arg1"])
print(params["arg2"])
if __name__ == "__main__":
sys.exit(main(sys.argv))

How to set shell=True for a Subprocess.run with a concurrent.future Pool Threading Executor

I try to use the concurrent.future multithreading in Python with subprocess.run to launch an external Python script. But I have some troubles with the shell=True part of the subprocess.run().
Here is an example of the external code, let's call it test.py:
#! /usr/bin/env python3
import argparse
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-x', '--x_nb', required=True, help='the x number')
parser.add_argument('-y', '--y_nb', required=True, help='the y number')
args = parser.parse_args()
print('result is {} when {} multiplied by {}'.format(int(args.x_nb) * int(args.y_nb),
args.x_nb,
args.y_nb))
In my main python script I have:
#! /usr/bin/env python3
import subprocess
import concurrent.futures
import threading
...
args_list = []
for i in range(10):
cmd = './test.py -x {} -y 2 '.format(i)
args_list.append(cmd)
# just as an example, this line works fine
subprocess.run(args_list[0], shell=True)
# this multithreading is not working
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
executor.map(subprocess.run, args_list)
The problem here is that I can't pass the shell=True option to the executor.map.
I have already tried without success:
args_list = []
for i in range(10):
cmd = './test.py -x {} -y 2 '.format(i)
args_list.append((cmd, eval('shell=True'))
or
args_list = []
for i in range(10):
cmd = './test.py -x {} -y 2 '.format(i)
args_list.append((cmd, 'shell=True'))
Anyone has an idea on how to solve this problem?
I don't think the map method can call a function with keyword args directly but there are 2 simple solutions to your issue.
Solution 1: Use a lambda to set the extra keyword argument you want
The lambda is basically a small function that calls your real function, passing the arguments through. This is a good solution if the keyword arguments are fixed.
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
executor.map(lambda args: subprocess.run(args, shell=True), args_list)
Solution 2: Use executor.submit to submit the functions to the executor
The submit method lets you specify args and keyword args to the target function.
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for args in args_list:
executor.submit(subprocess.run, args, shell=True)

Resources