Completing Your Stack Class & Measuring Test Coverage
In this lesson you’ll complete your Stack
class by implementing the pop
method, which includes writing tests for it.
00:00 Finally, we need to add pop functionality. We have the ability to push an item onto the stack, but now we need an ability to pull an item off, or pop an item off of the stack.
00:12
So, let’s go ahead and create that test function. def test_pop()
. And we’re going to dynamically inject in this stack
object—
00:26
from up here, because it’s a fixture. Now what we’re going to do is we’re going to take this stack
and let’s push, just like before, let’s maybe push a string, like "hello"
.
00:39
So we’ll push a string onto the stack
. Now, this already works because we did this—we did .push()
in a previous test, so we know that .push()
works. The next thing we want to do is pop—we want to pull something off the stack. So if I were to say—actually, maybe let’s push one more item. So I’m going to say stack.push('world')
. Okay.
01:04
So now let’s pull some items, or pop the items, off the stack. So I want to assert that if I do stack.pop()
, just like this, it should pull the last item off.
01:19
The last item is "world"
, so that should equal "world"
. And then if I’m to do another assertion…
01:33
then, now that "world"
is gone, then "hello"
should be next.
01:38
Okay. Now of course, you could stop right here, but you may be thinking to yourself, “Okay, but what happens if I do it again?” Well, what happens in a list is if you keep pulling items from a list, it returns something called an IndexError
. So we have to decide here—and this is just from the programmer’s perspective—when I do .pop()
and I’m at the end of the list, what should happen? Now, you can either do one of two things, generally speaking. You can return nothing—in this case, Python has a datatype called None
, so you could return that—or you could throw an exception saying that you’re doing something illegal. In this case here, what I want to do is just return None
, but yes, of course you could just throw an exception.
02:22
So, because the stack
is empty and because I want an empty stack if popped to return None
, I’m going to say assert stack.pop() is None
.
02:36
Okay? So this is our test. I put two things onto the stack and then I popped them off in reverse order. "world"
comes off first, "hello"
comes off second. The stack is then empty, and then if I do a .pop()
on an empty stack, then I want it to return None
. Okay.
02:50 That looks pretty good, so let’s go ahead and run our test.
02:54 So, if I flip back to here and I run my test, I get an error message saying, well, first of all, the constructor passed. Push passed, but pop is failing.
03:03
And pop is failing because this says there is no attribute 'pop'
. So let’s fix that error message—or, let’s fix that so the error message goes away.
03:14
So if I come up here and I type in def pop(self)
—now, does this .pop()
method need any additional parameters? And as you can see, by looking at this—this signature—that it doesn’t need any parameters. It’s empty.
03:29
So we can just leave that off, hit that, and all I need to do is just return
—so naively, this isn’t the right answer but let’s do a naive answer—is I could just return self._storage.pop()
.
03:48
Lists, of course, have an internal .pop()
method, so we can actually use that for our application. Okay. So let’s try this. There’s a slight problem though, in that when you pop off of an empty list, it actually does throw an error message, so these first two may pass, but this bottom one will fail.
04:08
Let’s try it. Back to our test, run it. And it looks like here, I got a failure, and the failure is it looks like "world"
passed, "hello"
passed, but this one right here failed.
04:21
And the reason it failed is because we got an IndexError
. We tried to pop from an empty list. So, that’s sort of what we expected to happen, and it did.
04:31
So what I want to do here is I want to do a try
/ except
block. So,
04:36
here’s try:
and in this try
, what I’m going to do is I want to say…
04:46
item = self._storage.pop()
. So, I’m going to try this. I’m going to try to pop this thing off, and if it works, go into this item
variable,
04:59
except when some exception happens. In this case here, we have an IndexError
. So, I’m going to type in IndexError
, just like that.
05:07
So I’m going to try to do this, if this fails and if it fails due to just this specific error, what I want to do is say item = None
. Okay? So, item
is good right here—if it pops successfully.
05:22
But if you happen to have an empty list and you pop from that, then you’re getting an IndexError
. Okay, so we’re either going to get something good or None
. And then at the end, I’m just going to return item
.
05:37 Okay. So let’s see what happens now.
05:42
And it passes. That’s excellent. We were able to successfully create a .push()
and a .pop()
, and pull and pop everything off, including to pop from an empty list and give us None
.
05:54 So that looks really good. We have our fixture, and so—yeah! This data structure is effectively complete. Once again, it’s a simplified version. You could add additional features to it, but the basic features any stack needs, of course, is push and pop, and we did implement that, so this is great. The last thing I want to do—and I mentioned this—is test coverage.
06:14
Like, how do you know how many lines of code you actually tested? And how you can do that is just you run your test and do --cov
, just like this.
06:27
--cov
. And what that’s going to do is it’s going to rerun your tests of course, but it’s going to show you all your files—your __init__
, stack
, and your test—and it’s going to show you the number of statements that you have in each one.
06:40
None in this __init__
file, 13
in stack
, and 19
in test_stack
. And how many statements got covered?
06:48 How many missed coverage? Coverage means—what does coverage mean? It means during the test process, how many lines of code did you actually execute during the test?
06:59
And so what this means is—we actually care about this right here, our actual Stack
class—and it looks like all 13 lines, or 100%
of the code we tested.
07:11 So not only A, did our test pass, but we have 100% test coverage. Okay. So, this concludes this video. Hopefully it was useful. Once again, this video is how to implement code or write an application using test driven development, where you actually write your tests first and the implementation second. Hopefully, this motivates you a little bit to try this in your next application. Thank you.
Chris on March 15, 2019
I may have missed it somewhere in the course, but the cov plugin was not installed by default on my windows machine while working in venv. I had to do a little googling to solve the error I received:
docs.pytest.org/en/latest/plugins.html?highlight=cov
Hope this helps someone in the future.
Rodrigo Vieira on March 17, 2019
Yes, definitively the “coverage” plugin is missing. We can simply install that by running:
pip install pytest-cov
Also, I don’t know why but all my “site-packages” files are getting returned by python -m pytest --cov
. Does anyone know how to return only my project files as it’s returned on video lesson?
Isaac Dadzie on March 20, 2019
You can run the command.
python -m pytest -v --cov=ds
OR
Create a .coveragerc
file with following
[run]
source = ds
Your directory structure should look like this.
-rw-r--r-- 1 user user 17 Mar 20 12:14 .coveragerc
drwxr-xr-x 3 user user 4.0K Mar 20 11:25 ds/
drwxr-xr-x 3 user user 4.0K Mar 20 11:37 tests/
Martin Breuss RP Team on July 17, 2019
Nice! Thanks @Chris, @Rodrigo Vieira and @Isaac Dadzie for filling in the last missing pieces and pointing us forward here :)
Lokman on Feb. 19, 2020
Hello, why class constructor in push function don’t have return keyword like the others?
def push(self, item):
self._storage.append(item)
Ricky White RP Team on Feb. 21, 2020
Hi @Lokman,
Functions don’t aways have to return something. In this case, the push
function only appends an item to _storage
. We don’t need to have it tell us anything once it’s done. Unlike def __len__()
where we want to know what the length is. Hence this returns the length of the current Stack
to us.
Hope that helps.
Lokman on Feb. 23, 2020
Thanks @Ricky White for the answer.
Wesley on Aug. 7, 2020
This guy is great. A lot of times when watching videos, I have a question in my head that never gets answered. This guy guessed my questions, spoke them out loud and then answered them not once but twice.
Kudos for not leaving out important details.
Would like to see a pytest assertion that the proper error is thrown/raised.
carl on Aug. 26, 2020
It might have been helpful to demonstrate how to write a test for a call to a method that is expected to throw an exception. For example, instead of returning None
from the pop
method when the stack is empty (which we wouldn’t want to do if we actually want to be able to push None
onto the stack) we might want to let the IndexError
propagate instead of catching it, or perhaps raise our own custom StackUnderflowError
.
Assuming we just want to let the IndexError
propagate, one way to test what happens when the stack is empty (assuming the same fixture setup) would be something like this:
def test_pop_when_empty(stack):
try:
stack.pop()
assert False
except IndexError:
assert True
Is there a more concise approach?
Ernie White on Dec. 14, 2020
@Carl
From my understanding, something like this should be used for exception testing.
def test_pop_when_empty(stack):
with pytest.raises(IndexError):
stack.pop()
Darek on May 8, 2021
Or even something better 😉:
def test_pop_when_empty(stack):
with pytest.raises(StackEmptyError):
stack.pop()
since raising a custom exception in this case would be advisable. This custom exception can wrap the original one by using the raise ... from OriginalError
construct.
ssunkara on June 16, 2022
The parameter stack passed to the test functions is confusing. stack is technically a fixture function which returns an instance of the class Stack.
This is where it gets confusing for me: .push() and .pop() methods are called on the fixture stack and not Stack
Can you help elaborate on the syntax?
Thanks.
pam on June 19, 2023
You need to install pytest-cov then
jchurchi on Aug. 30, 2023
@Chris It helped me buddy 👍 !!
I was going to ask about this error I got this from using the –cov argument (and now I don’t have to). ERROR: usage: main.py [options] [file_or_dir] [file_or_dir] […] main.py: error: unrecognized arguments: –cov inifile: None rootdir: C:\Users\<jchurchi (my user)>\dev\real_python\testing\ds
Become a Member to join the conversation.
ypochienTW on March 13, 2019
Simply and clear. Nice course for pytest newbie.