Writing Your First Test & The Core TDD Cycle
After setting up the project, you’ll now write your first unit test and learn about the core TDD cycle.
00:00
To start, let’s just go ahead and create a skeleton for our stack data structure. To do that, I’m going to create a class called Stack
. And since I don’t have any particular implementation, I’m just going to type in pass
.
00:17
So, this will at least be syntactically correct if I try to run it. Next, I would like to import this Stack
class into my test file. So, normal import rules apply—I’m going to say from ds.stack import Stack
.
00:41 Now, what I can do is run this. Now of course, there are no actual tests, but that won’t prevent us from running it. I’m going to switch over to the terminal and I’m going to run this and just see what happens.
00:54
So, how you can run pytest
is you type in python -m
—which means module—pytest -v
. And -v
means verbose, so, “Show me all the particular tests that are either passing or failing.” And of course we can see here, it says collected 0 items
—that means there were no tests—and we can see that there were no tests, and this test ran in 0.01
seconds.
01:17
So this is showing us that pytest
is working, it is operational. It’s just—there were no tests that it saw. That’s okay, we can add some.
01:29 Let’s go ahead and add a test for our constructor. So, we might want to go ahead and construct an object from this class, and that’s called a constructor. Let’s create a test around that.
01:39
If I were to create def
—which is, “I’m going to create a function”—I’m going to, call it test_constructor()
, just like that. And so how pytest
works is the name of the function should be prepended with test_
(test
underscore). If you were testing a dog, you would say test_dog()
. If you were testing a cat—test_cat()
.
01:59
So, that’s just how it works. Now, because we’re testing a constructor, I’m just going to call this test_constructor()
. Now in this method right here, I need to create an object.
02:08
I’m going to call this object s
and it’s going to come from this Stack
class. Now, once you have this object, you can make what’s called an assertion against it. And what assertions are—it’s a keyword, assert
, and what happens after it… Let’s imagine I say 1 == 2
. This, what assert
is looking for, is a Boolean expression. 1 == 2
will evaluate to either a True
or False
. If it’s True
, the assert passes.
02:41
If it’s False
, the assert fails. So here, because 1
clearly does not equal 2
, this test actually will fail. So if I come back here and run this test again, we can see down here it says assert 1 == 2
—that was an error. It happened because 1
does not equal 2
, and it says 1
test failed. So, how you can make this test pass, of course, is you could type in 2 == 2
or 1 == 1
.
03:08
We can flip back here and run the test again. And it says 1 passed
and it’s in green. The test_constructor()
actually passed. So, this is how you can actually test out something—whether it works or not—using these assertion statements.
03:24
So instead of using 2 == 2
, I can change this to say isinstance()
. Is what an instance? Is s
—this object that I just created—is that an instance of Stack
.
03:36 That’s maybe one assertion that I can make—I can make multiple assertions, but let’s just start off with a very simple one. Okay. I’m going to flip back to the terminal…
03:50
and that passed, and that should make sense, right? You can see right here that s
is actually coming from Stack()
, so this assertion should pass.
04:00 Now, the question you want to ask yourself is, “Is this a good test?” And I would say “No.” The reason is because when you create a stack, it’s actually a data structure that contains internal data—like a list contains data, or a set contains data, or a dictionary contains data.
04:17
There’s nothing in this Stack
over here that actually contains data. And so what we can do is let’s go ahead and override our constructor so it actually will contain some data, and then we can test it.
04:29 Now, this is called test driven development, and it’s called test driven development because you should actually do the tests first. And so what I would like to do is just go ahead and write our tests.
04:38
I’m going to say assert
, and the test is going to fail and it should fail—that’s the whole purpose behind it, is a test will fail and then we’ll make it pass. And in the process of making it pass, we are essentially adding functionality to our Stack
class.
04:54
Okay. What I’d like to do is I would like to say “Is the length of s
,” which happens to be a Stack
, “is it 0
?” Is the length of this Stack
0
?
05:10
So, this is pretty strange. Normally, you take the length of a list or length of a tuple, for example—but how can you take the length of a Stack
? That may not seem to make any sense.
05:20 And of course, this test will fail.
05:24
If I come back to my terminal and run the test, we can see that it failed, and the reason it failed—it says the Stack
has no function called len()
.
05:32
So, we’re going to have to do a couple things. One is we’re going to have to create a function called len()
—so maybe we’ll do that first.
05:42
In order for this len()
function to work, inside the Stack
we have to create a magic method called called .__len__()
.
05:51
self
is the parameter. And then we can just return—obviously, .__len__()
should return some numeric value, like 5
or 7
or whatever. Let’s just say return 3
, just so the test can move forward.
06:08
If I save this and I flip back to here and I run my test again, last time it gave me an error and it said len()
was undefined. Now it says that 3
doesn’t equal 0
, because remember, I’m actually returning 3
from that test.
06:25
And of course, I could actually just return 0
and the test would pass, but that’s not true to what I’m trying to do here. So how can I make this .__len__()
return a number 0
and actually do what I want to do, which is contain some internal data? And how I’m going to do this is I’m actually going to create a constructor method.
06:45
And so I’m going to say def __init__(self)
. So this is my constructor method. And in here, what I want to do is I’m going to say self._storage
is equal to an empty list.
07:01
So, this is going to be the internal storage inside of my Stack
data structure.
07:08 So if I put in dogs or cats or people or numbers or Booleans or whatever—I need someplace to actually store that, and so that will be stored inside here.
07:17
Now, you may be wondering why am I calling it _storage
instead of just storage
. Well, sort of by doing an underscore (_
), I’m relaying information to potential other programmers—or maybe my future self—that this attribute should be private.
07:35
Okay? I’m signaling that I want this to be private, because there actually isn’t private
in Python. You have to signal it with these underscores.
07:45
So, I don’t actually want to expose ._storage
to the external world. I want it to be private, I want to be internal, so that’s why I’m, prefixing _storage
with the underscore (_
). Now—now that I have this—what I can do in my .__len__()
function is instead of returning 3
, I can just say return the length of self._storage
.
08:09
And because ._storage
is actually a list, I’m effectively saying, “What is the length of this list?” The list is 0
—there’s nothing inside the list, because it’s brand new.
08:19
And so this will return a 0
, and therefore, when I do len()
over here—len(s)
—len()
actually calls this .__len__()
—the double underscore .__len__()
.
08:30
Okay? And so that should return 0
. So, hopefully this test will pass. I’m going to switch back to my terminal and let’s see what happens. So here I am in the terminal, I’m going to clear my screen and rerun the test—and it passed.
08:46
And that’s exactly what I wanted to happen. So, in recap, I create an object called s
from the Stack
class. And what happens is I create some internal storage that’s private, that’s a list. And it’s empty—it’s an empty list.
09:01
And when I happened to call len()
on this Stack
, it actually calls this .__len__()
, and it returns the length of this actual list, which happens to be 0
, and therefore this assertion passes, and the assertion on line 6 passes as well too, because s
is a Stack
.
09:19
So, this is the first step in actually creating our Stack
class.
Elie Kawerk on March 13, 2019
Thanks @Dan, is the material covered in the video available on Github?
BTW, I like the revamped real-python platform a lot!
Dan Bader RP Team on March 13, 2019
Not right now but that’s a great idea, we can definitely put the sample code up on GitHub and then link it from here :)
Vikram Kalabi on March 18, 2019
Could you please share information on your Visual Studio theme and setup? Looks wonderful!
Dan Bader RP Team on March 18, 2019
@Vikram: Here they are—
The theme is Monokai Pro. www.monokai.pro/vscode/
I use either Dank Mono or Operator Mono. dank.sh www.typography.com/fonts/operator/styles/
I copied this from one of Chyld’s comments on another video in this series.
Grant King on March 20, 2019
That import line gives me an error with this file structure, but everything works if I move test_stack.py directly under the DS folder. Is there a better way to make it work?
Vanam on April 12, 2019
Nice tutorial, terminal that is used is it ZSH?
adoormouse on Sept. 10, 2019
Had issues with running pytest with this setup. Got it working with:
test_stack.py
from ds import Stack
...
(venv)$ python -m pytest tests/
AugustoVal on Sept. 18, 2019
Quick question - I am getting this message trying to run Pytest
AVal-iMac% python -m pytest -v
/usr/bin/python: No module named pytest
AVal-iMac% python3 -m pytest -v
/usr/local/bin/python3: No module named pytest
I have the package installed according to my list.
AVal-iMac% pip3 list
Package Version
------------------ ----------
pytest 5.1.2
pytest-cache 1.0
pytest-pep8 1.0.6
Any subjection why it is not working for me? I already un-installed and re-installed but is not working.
Thanking you in advances for your help
Dan Bader RP Team on Sept. 18, 2019
@AugustoVal, try running the pytest
command directly like so:
pytest -v
Dri on Oct. 2, 2019
I’m getting the following error when running pytest:
ImportError while importing test module '/test-driven-dev/tests/test_stack.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
tests/test_stack.py:1: in <module>
from ds.stack import Stack
E ModuleNotFoundError: No module named 'ds'
My directory schema:
test-driven-dev ]$ tree
.
├── ds__.py
│ └── stack.py
└── tests
├── __pycache__
│ ├── test-stack.cpython-37-pytest-5.2.0.pyc
│ └── test_stack.cpython-37-pytest-5.2.0.pyc
└── test_stack.py
│ ├── __init
Any one gone through this?
przemodev on Nov. 20, 2019
I believe that saying that a single underscore makes variable private and protects from accessing the value directly does not make any sense. Would rather say that __var makes the variable somewhat private i.e: not accessible by just a . notation on the object (__var is called _ClassName__var instead and . accessible anyway).
Christopher Lee on May 5, 2020
Never in the history of forever has anyone ever picked such a terrible font to due a coding tutorial, it’s like a horrible Walt Disney style of text that I cannot read…
subonlinetmp on July 29, 2020
@Dri Yes, me too if I try just pytest -v
(Though your problem may be that __init.py__
should be in ds/
not tests/
– as ds is meant to be the package from which one can import the module stack with the Stack class. However, supposedly in Python 3.3+ __init.py__
is no longer required…but, maybe you’re running an earlier version and this causes ds to not show up as a package for you?)
@Dan Bader First off, I don’t understand why we’re not running pytest directly as you suggest (i.e. why he’s using python -m pytest -v
to start with, instead of just pytest -v
as you suggest)…?
Indirectly, maybe this is why: When I attempt to run pytest directly, I get the same error @Dri reported:
12:08 ~/src/python/examples/tdd} pytest -v
============================= test session starts =============================
platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.1, pluggy-0.13.1 -- /Library/Frameworks/Python.framework/Versions/3.8/bin/python
cachedir: .pytest_cache
rootdir: /Users/rich/src/python/examples/tdd
plugins: cov-2.10.0
collected 0 items / 1 error
=================================== ERRORS ====================================
____________________ ERROR collecting tests/test_stack.py _____________________
ImportError while importing test module '/Users/rich/src/python/examples/tdd/tests/test_stack.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
tests/test_stack.py:1: in <module>
from ds.stack import Stack
E ModuleNotFoundError: No module named 'ds'
My import is same as in the course:
from ds.stack import Stack
My file structure is also the same:
12:08 ~/src/python/examples/tdd} tree
.
├── README.txt
├── README.txt~
├── ds
│ ├── __init.py__
│ ├── __pycache__
│ │ ├── stack.cpython-38.pyc
│ │ └── test_stack.cpython-38-pytest-5.4.3.pyc
│ ├── stack.py
│ └── stack.py~
└── tests
├── __pycache__
│ └── test_stack.cpython-38-pytest-5.4.3.pyc
├── test_stack.py
└── test_stack.py~
I’m glad about this error, actually, as it points out something I don’t understand, but feel is important: I’d like to understand how the import is searching for the ds module in general, and why it’s not finding it with “pytest”, but does find it with python -m pytest
…?
From web sleuthing, I’ve read that the latter adds CWD to sys.path
. But, I still don’t see how it all fits and why one works and the other doesn’t? Why would it need to add CWD to a path when I’m running from that same dir? Is it searching from tests/
since it finds test_stack.py
there, but no ds/
subdir in tests/
?
To test that, I tried adding CWD to --rootdir
, but got the same error:
01:15 ~/src/python/examples/tdd} pytest -v --rootdir=$HOME/src/python/examples/tdd/
============================= test session starts =============================
platform darwin -- Python 3.8.2, pytest-5.4.3, py-1.8.1, pluggy-0.13.1 -- /Library/Frameworks/Python.framework/Versions/3.8/bin/python
cachedir: .pytest_cache
rootdir: /Users/rich/src/python/examples/tdd
...
tests/test_stack.py:2: in <module>
from ds.stack import Stack
E ModuleNotFoundError: No module named 'ds'
…so, I added an assert to test_stack.py
to check sys.path
and found that, sure enough, the difference is that CWD = /Users/rich/src/python/examples/tdd
on the sys.path
(and that --rootdir=$HOME/src/python/examples/tdd/
does NOT add it to sys.path
!!)
via python -m pytest -v
:
sys.path = ['/Users/rich/src/python/examples/tdd/tests', '/Users/rich/src/python/examples/tdd', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python38.zip', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload', '/Users/rich/Library/Python/3.8/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/gnureadline-8.0.0-py3.8-macosx-10.9-x86_64.egg']
via pytest -v
:
and
pytest -v --rootdir=$HOME/src/python/examples/tdd/
:
sys.path = ['/Users/rich/src/python/examples/tdd/tests', '/Library/Frameworks/Python.framework/Versions/3.8/bin', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python38.zip', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/lib-dynload', '/Users/rich/Library/Python/3.8/lib/python/site-packages', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages', '/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/gnureadline-8.0.0-py3.8-macosx-10.9-x86_64.egg']
So, I’m at a loss to figure out just what’s going on here (unless there’s a bug in pytest --rootdir
), nor how to get pytest to work directly as suggested…
[ other than this symlink hack in tests/:
01:29 ~/src/python/examples/tdd/tests} ls -l ds
lrwxr-xr-x 1 rich staff 5 Jul 29 01:28 ds@ -> ../ds
]
I’d really appreciate some help with this!!
subonlinetmp on July 29, 2020
PS. After reading docs.python.org/2/tutorial/modules.html#the-module-search-path, I found that this also allowed pytest -v
to work (ie, find the ds.stack module):
01:35 ~/src/python/examples/tdd} export PYTHONPATH="$HOME/src/python/examples/tdd/"
But, that also seems like a hack: In order for this to work automatically, we’d need to be setting PYTHONPATH
every time we change directories!
Why doesn’t pytest just include CWD by default? Why doesn’t --rootdir=CWD
work either?
(Maybe the author’s already been down this path!? All the sudden python -m pytest
isn’t looking so bad! :)
Julie Stenning on Sept. 27, 2020
On my Windows system, the import statement in the test_Stack.py file didn’t work because it couldn’t find the module “ds”.
I resolved it by using steps covered at python-packaging.readthedocs.io/en/latest/minimal.html. However, it isn’t a perfect fix because I couldn’t tell it to just import the class Stack. It did allow me to carry on with the tutorial.
Julie Stenning on Sept. 27, 2020
I don’t appear to be able to edit my own comments. I was too hasty. Because I can’t refer to the class properly in the test file, I am unable to carry on. I will be able to if I put the module stack.py and the test module in the same folder. So .... ignore my comment above.
Darek on May 8, 2021
Hi there. I’d like to kindly point out that the __init__
method should not be called “constructor” but “initializer.” In Python, constructors are rarely used in day-to-day programming and when one starts using them, one does something that’s called meta-programming. Writing an object constructor requires writing code for the type/class of the object, not the object itself. Just wanted to clarify.
eulle100 on Jan. 31, 2022
Could you please share information on your Terminal theme and setup? Looks wonderful!
Lucas Zago on Nov. 3, 2022
I believe some people have the same issue when running pytest:
from ds.stack import Stack
E ModuleNotFoundError: No module named 'ds'
How to solve it?
Bartosz Zaczyński RP Team on Nov. 3, 2022
@Lucas Zago Can you show us your directory structure, e.g., by running the tree
command as shown in the previous video?
Ariba S on July 8, 2024
was the import error question answered? I get the same issue…
Martin Breuss RP Team on July 9, 2024
@Ariba S can you share your directory structure, like @Bartosz Zaczyński suggested? It’ll help to debug what might be going wrong with the imports.
Become a Member to join the conversation.
Dan Bader RP Team on March 13, 2019
@Elie: Thanks for your question. Our video lessons are only available for streaming at the moment.