Testing is good for you and your code, but sometimes can be tricky!
A while ago, as a newbie programmer, who just finished his first course, the only thing I knew than about testing, was a big mention made by Cisco Networking Academy or Python Institute itself, claiming that unit testing is a must, is broad and complex topic, and that it will be covered in separate course...
There even is a site on Python Institute dedicated to this topic, but as for present day (01.11.2023) no course is available there... but there is a chance, because they give us a time window when this course should be available (https://pythoninstitute.org/python-for-testing-essentials)!
So while undertaking a couple of courses I stumbled upon Pytest library, and scratched UnitTest. The former was more broadly described during CS50P course brought by Harvard University, the latter was somewhat described in one of courses I've undertaken on Udemy so far. For now let's stick to Pytest, as I have more experience with it.
How to test functions with input and without return statements?
The short answer is, with ease 😂, but the longer one is one with most interest, let us begin.
1. Testing functions taking needing user input to work
The general rule is that in Python everything is an object, and if so, everything has attributes which can be accessed and changed. Pytest is equipped with monkeypatch fixture, designed for manipulating global settings and environment. Monkeypatch has setattr() function for details check their documentation (https://docs.pytest.org/en/7.1.x/how-to/monkeypatch.html) which takes object/target, name and value as parameters like so:
setattr(obj, name, value, raising=True)
obj ➔ is an object we want to reffer to. Can be an object or a string representing dotted import path for instance 'os.getcwd'.
name ➔ is that object's element we want to change for instance input function. Has to be a string.
value ➔ what do we want to set above to.
raising ➔ Raises AttributeError if the attribute does not exist, unless raising is set to False.
Let's assume that we have a simple function in file called name.py which returns input's variable:
So to conclude proper test, we need to somehow bypass the usual way of input, and siply put something in place, where our function is going to be able to read it.
As far I know there are two possibilities, one is the input function itself, other is system's stdin where all input is being streamed through.
Let's start with the input function itself.
There is builtins module in Python in which are all built in functions (https://docs.python.org/3/library/functions.html and https://docs.python.org/3/library/builtins.html#module-builtins). So in general we should be able to access it as builtins.function_name() -> builtins.input(). The important thing here is that the input() function takes argument, so we will need to build our code in a way, that represents that argument.
Following the setattr mentioned earlier our line looks like this:
monkeypatch.setattr('builtins.input', lambda _: "MrSz-84")
Where 'builtins.input' is dotted notation of exact function Pytest should look for and bypass it's value. Why wait for the user, when no user is needed? Notice that our lambda function has _ as an variable, and it is a must. This variable is not used by us, but the input function needs it as a parameter, and our code wouldn't work without it. Also 'builtins.input' is a shortened way of using setattr(). So let's take a look at the whole code block.
STDIN approach.
As I've mentioned earlier, this is a stream of data which system uses for reading data from outside world 🌍 and users 👽. The idea is to use sys.stdin() and replace what should be in readline/readlines function (more at https://docs.python.org/3/library/sys.html#sys.stdin). Important thing to remember while using this approach is that stdin takes no arguments, and if so our function can't pass one.
Following the setattr mentioned earlier our line looks like this:
Here is an example of object, name and value params. Notice that our lambda function has no variable in it, and this is because stdin takes no arguments. If we would use _ like in the previous example Python would throw an exception at us. So the whole code block looks like this:
And the result after Pytest run:
2. Testing functions with side effects - no return statement
So now let's go a step further and check how we can read the what is being sent to the screen. At this point it should be obvious that if there is a stdin stream, there has to be stdout stream which sends information to screen. So the idea is to hook to it, and read what is being sent to print function for instance.
Our modified sample function looks like this:
The first step is to bypass user input, and both cases look a bit different in the way what is going to be redirected by our code. But we need some theory for starters. How to redirect a stream sent to our print function? We need two libraries for that purpose. Contextlib and IO. Contextlib is going to open a stream for us and redirect its contents to the place of our choosing, variable, file, object, etc. (https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout).
contextlib.redirect_stdout(new_target)
But we need one more thing to make this work, mainly a way to read stream contents. And here IO module comes into play, and being precise its StringIO class ➔ (https://docs.python.org/3/library/io.html#io.StringIO)
class io.StringIO(initial_value='', newline='\n')
builtins.input
While using this method of bypassing user's input, the output is going to be only waht input function returns, in our case MrSz-84.
After the bypass we need to start the context manager using with
statement, and then copy paste the example from contextlib link and
redirect the stdout to our StringIO class which is going to be saved as
an instance/object named buffer. Inside the context
manager our tested function needs to be placed, after what we can
extract the contents of redirected stdout stream, using getvalue()
method on buffer object. Stripping helps get rid of newline char \n. The whole code block looks like this
STDIN approach.
This one is a bit different due to the length of the stream captured, which would be equal to this: State your name: MrSz-84\n. So the only real difference, aside from bypassing lines, is going to be extracting the last part after the colon sign, and we are done❗
The code block is as follows:
And Pyetst results:
I hope this tutorial will help you in testing, and from now on you are going to be able to check, how all your functions behave in controlled environment.
If you found this helpful please share and leave a comment. Inf not, definitely leave a comment, and write what would there needed to be changed, and what did I miss.


Komentarze
Prześlij komentarz