[AUDIO LOGO]
CARTER ZENKE: Well, hello one and all.
And welcome to our short on pytest.
Now, pytest is a module you can use to test your code more thoroughly
than you could on your own.
And I have here a program called convert.py,
whose purpose is to convert this unit called an astronomical unit, often used
in space exploration and so on, into a more friendly measurement called
a meter.
So I have here down below the convert function.
And this does the following.
It first checks if the argument au is of type int or float.
And if it is not, it raises, in this case, a TypeError for the wrong type.
It says "au must be an int or float."
In this case, if it is not.
Otherwise, if au is of type int or float,
well, we'll go ahead and return the conversion between astronomical units
and meters from this function convert.
Now, I could go ahead and run Python convert.py,
and I could maybe test this on my own.
I could type in maybe 1 for AU and get back,
in this case, some large number of meters.
I can go ahead and type python convert.py and get back, in this case,
again, some large number of meters.
But I could probably be more thorough than this.
And I would really hate to be the programmer
to sit down and write these tests myself just kind
of hitting Control-L to close my terminal,
run this program again, type in some new input, and check the result by hand.
So that's why things like pytest exist in the world.
Now, if I want to write some tests for my code and do so with code itself,
I can use by convention, per pytest, a file
called test underscore and then some given name.
So I'll call this file test_convert.py to test, in this case,
this function I have called convert.
And again, by convention, in pytest, files that hold tests for our code,
in this case, have to begin with test_.
So I'll go ahead and open up code of test_convert.py.
And again, by convention, I'll go ahead and import this module called pytest.
Now, what could I do?
Well, I want to, in this case, test my convert function.
In general, when we're using py test, we're engaging in unit testing,
testing individual units or functions in our program.
So I'll save main for testing later on.
I want to focus on convert right now.
So I could go ahead and import the convert function from my convert file.
I could type from convert, this file here, import the convert function.
And now I can use the convert function inside of test_convert.py.
Well, here, again by convention, if I want to write some code to test convert,
I should write it inside of a function that begins with test_.
So maybe the simplest thing to test here is just some simple values,
like the conversion between 1 AU and 2 meters instead.
So I'll type def and then test_conversion here.
And this is making for myself a new function
that will run when I call pytest in my terminal.
As we've seen before, pytest relies on this idea
of asserting something to be true.
And if it is not, pytest will let us know if a program has not
behaved as we expect.
So here, I could assert, let's say, that when I call convert and give it
an input of 1, that should be equal to, in this case,
this large number of meters.
I'll go ahead and type this in here, 149597870700.
Let me double check this here, 149597870700.
So this is a large number of meters, but it is
the conversion between 1 AU to meters.
Now, a side note here is that if I was running these tests,
I'd probably want to be very sure this is the right conversion.
This is probably a good chance to say if you
had to type out-- what is this, 1, 2, 3, 6, 9, almost 12 numbers here,
just be doubly sure you're typing each of them
right when you're writing tests like these.
So here, I'll go ahead and run pytest now that I have, in this case,
my test_convert.py file.
And when I run pytest in my terminal, it will look
for a file like this, test_convert--
or begins with test_.
It will then run all of the functions that begin with test_ and let me know
how many have passed and how many have, in this case, failed.
So I'll go ahead and run pytest.
And fingers crossed, we see that one has passed.
In test_convert.py, there is one little dot here,
meaning this function did what it was supposed to do and did not encounter any
errors.
Now, it's a good idea to think through other kinds of scenarios
to test in your code.
In this case, I'm converting 1 AU.
But what are some other representative scenarios we could test?
Maybe something like 50 AU as well-- convert
50, which happens to be this other long string of numbers.
So again, message here is maybe write your test
so you get fewer numbers than this--
3535000.
And I'll double check this again, 74798935335000.
Great.
All right, so this is the conversion between 50 AU to meters as well.
I'll go ahead and run pytest again.
And fingers crossed, we see that, again, those tests have passed.
And notice here, it still says "1 passed" down below, 1 passed.
We count, in this case, by the function.
But in this case, these two individual tests
have to both pass for this test to be considered passing as well.
Now, we could think of other kinds of scenarios
to test, in this case, negative numbers, maybe 0 as well.
Things like that are really useful for testing your code here.
But more on unit testing in lecture.
We'll focus here mostly on pytest.
So here, what else could we do?
Well, we've seen that in convert.py--
that in convert.py, I have, in this case, not just the pathway
of returning the actual conversion but also
this pathway of raising a type error.
And it's, again, a good idea to test all these pathways through our functions.
So we could ideally try to test, does convert raise a TypeError
when given an input of the wrong type?
Well, thanks to pytest, we can actually do this.
I'll go ahead and go back to test_convert.
And let's go ahead and try to add in, in this case, a new test, one that tests
for the proper error being raised.
I'll call this one test_error.
And again, we'll make sure this begins with test_.
I can then use what's called a context manager with here
to go ahead and try to run convert and expect, let's
say, that I get a TypeError, in particular.
So by convention with the syntax of pytest,
I could type "with pytest.raises TypeError" colon.
And indented inside of this with block, I'll
type convert and maybe give as input parentheses quote 1, just like this.
So this now means that if I were to call convert and give as input "1," quote,
unquote, well, I would expect a TypeError to be raised.
So slightly different syntax from what we see on line 7 and 8
but the same kind of semantics.
We expect whatever we run inside of this block here will raise a TypeError.
I'll go ahead and run python convert dot--
or actually, just pytest.
I'll hit pytest here.
And now we'll see that two tests have passed, so
not just test conversion, but also test_error as well.
So it seems like convert does raise a TypeError when given in this case
an argument that is not an int or a float.
Now, there's still more to test.
In fact, up here, I was really just testing whole numbers, 1 and 50,
but it's worth thinking of floats as a special case when we're testing.
In particular, this is because of floating point in precision.
So you've probably heard that there are only
a certain number of bits we can use to represent certain numbers in code.
In fact, in Python, maybe have anywhere between 16 bits to 32 bits to 64 bits.
But however many number of bits we have, we're
always going to have some finite number of bits to represent numbers.
When it comes to floats, there are actually
an infinite number of floating point values out
in the real world, any number of real numbers.
And so at some point, we actually can't represent decimals completely precisely.
And so when testing them, when testing inputs and outputs and testing floats
in particular, we have to make room for something
called tolerance or approximation, some bit of wiggle room to say that,
yes, this value here is 1.000000001, but it's close enough to 1
that we'll consider it as 1.
Let's go ahead and try and see what kind of utilities
pytest has for those kinds of scenarios.
I'll go ahead and look at a new test here,
one called def test_float_conversion.
And for consistency, I'll say this is testing our integer
conversion between whole numbers and whole numbers.
But this here is testing our float conversion.
And here, I'll try to assert that I will be
able to convert 0.001, a pretty small float itself, to some value
like this, 149597870.691.
Double check this here, 149597870.691.
So here, I'm essentially saying I expect that 0.001,
what we have as input to convert, will be
equal to this particular floating point value here.
But this might not always be the case depending on the level of precision
I'm expecting.
So if I want to allow for some kind of tolerance,
let's say I'll accept this number, 0.690000001 as also a valid equivalency
here, I can use pytest.approx--
pytest.approx and give as input the value I'm hoping to compare.
And this will use pytest's sensible defaults to allow for some tolerance
when testing the equivalency of these two floating point values,
both from the return value of convert and this one I've written over here.
So if I run pytest again, I'll see that three of these tests
have passed, which seems pretty good here.
But let's play around a little bit with the actual tolerance
that approx gives us here.
I could, if I wanted to, change the tolerance,
maybe make it tighter or a little looser.
Now, if I wanted to allow any value plus or minus,
let's say, 0.1, I could type this, abs equals 0.1.
And this means that if convert 0.001 returns to me this number
plus or minus 0.1, I'll accept it as being
equal to this particular number here.
So a bit of tolerance we've given ourselves here.
I'll go ahead and run pytest.
And we'll see that seems to have passed.
But let's see how tight we can make this tolerance
before we get into some trouble.
I might type something like this, maybe 1e negative 5, let's say.
So this is the same as writing 1 times 10 to the negative 5,
a very, very small tolerance here.
I'll go ahead and say pytest and hm, we seem to have gotten an error.
It looks like comparison failed.
We see here that the actual result obtained from convert 0.001
was this number here, which is not within the range of our expected value,
plus or minus 1.0 times 10 to the negative 5.
So if we loosen our tolerance a little bit, I could do 1e negative 4.
Pytest again.
Even that doesn't seem to work quite as well as we want it to.
But if I do 1e negative 3, fingers crossed--
even that doesn't seem to do--
1e negative 2.
That seems to be OK.
So lesson learned here is that you can actually
adjust these tolerances to be exactly the way you want them to be.
And what I mean by that here is that if I were actually willing to accept
this tolerance, this would be great.
But I might be writing some scientific application,
like maybe one for space exploration and so on, in which case a tolerance of 1e
negative 2 is really not going to cut it.
And so if I were to make this maybe 1e, let's say, negative 12, for instance,
a much more strict tolerance, and I see that this test doesn't pass, well,
I should probably do something in my code to make sure that I get a value
within this particular tolerance.
So you probably shouldn't do what I had just done here,
which is adjust the tolerance until you get the passing test.
You should probably set your tolerance first and then
make sure your code is giving you the right value you're hoping for here.
In this case, I'll leave this alone.
I'll say for now that a 1e negative 2 tolerance is sufficient enough for me
here.
But in the future, I want to make this a little bit stricter.
So here we've seen various features of pytest.
We've seen how to do simple asserts and testing for equality.
We've seen how, in this case, we can test for raising
certain errors, exceptions, and so on.
And we've also seen how we can test conversion,
how we can actually go ahead and compare floating point values using
pytest.approx.
This was our short on pytest.
We'll see you next time.