This is part three of a series about developing and testing a simple REST interface. Code can be cloned or viewed from https://github.com/tscott8706/restexample.
I encourage you to follow along in this series if you want to learn any of the following:
- TDD or unit testing in general
- REST implementation using Python
- My normal development process – this is the process I use to develop code, as well as my thought process!
My Development Environment Setup using TDD
If you are unfamiliar with TDD, go look at my post on TDD first. Got a grasp on what TDD is? Great! Continue reading.
In order to do test-driven development, I need to be able to run unit tests as well as write my source and test code. Several IDEs allow you to do this, but I personally use VIM. Since the method I use to run unit tests is just run inside a terminal, you can use whatever editor you want and run the tests as a separate process.
I use the split feature in VIM so I can show both the source and test code side-by-side at the same time.
Since I primarily work on my laptop at my house, I can’t show another terminal on the monitor without losing some space for my code editing. For this reason, I use screen in order to quickly change between the code and the test runs. Using screen, I can swap from code to unit tests with Ctrl+A then ‘n’. Using that hotkey again takes me back to my code. (FYI – Since doing this, I’ve swapped to tmux, which behaves similarly but has plugins to give me more functionality.)
Recall from a previous post, that in order to run unit tests, you can view the readme file in the project. At the time of this writing, it is a docker-compose command. That Docker instance will continuously monitor the filesystem (or the project) for code changes and run unit tests when it spots one.
A better way would be to just move the command prompt running the unit tests onto another monitor, so you can always immediately see the results of your source or test code changes without swapping windows/panels/workspaces/whatever.
Now that I have the development environment down, let’s look at some of the individual TDD cycles I used for this project!
I’m going to format this as a series of 1-2-3 steps of (1) writing the unit test, (2) writing the code, then (3) refactoring.
Note that these cycles occurred after I created the Python package structure and had restexample be a command-line executable to execute main.py. I did this through manually installing the package through a Docker container. Once that worked, I started with the TDD cycles.
- Cycle 1: I need a function that creates the Flask application (that way I can easily mock out the creation of the app).
- Let’s start with testing the create_app function (test_main.py):
12def test_create_app_creates_Flask_app(self):self.assertTrue(isinstance(create_app(), Flask))
- That function doesn’t exist yet, so write code in main.py to implement it.
12def create_app():return Flask("restexample")
- Let’s start with testing the create_app function (test_main.py):
- Cycle 2: now I need to make sure that main calls the “run” function on the app. That means I need to mock some things…
firstname.lastname@example.org("restexample.main.create_app")def test_main_starts_app_on_port_5000(self, mock_app):main()mock_app.return_value.run.assert_called_once_with(host="0.0.0.0", port=5000)
This code makes the create_app function return a MagicMock() object. These objects can see if they got called. In particular, create_app returns a value (the app itself). I then want to make sure “run” was called on that app. So mock_app.return_value.run is the run function of the app. assert_called_once_with does exactly what the function name states… asserts that the function was called one time with the given arguments. This test fails because main doesn’t exist yet.
- Let’s write main!
123def main():app = create_app()app.run(host="0.0.0.0", port=5000)
The test now passes, because the app has called run with the given arguments. We now have a running web server!
- No duplicated code or any complex logic at this point, so no need to refactor.
- Cycle 3: the goal is to eventually get a Person object to be inserted into a MongoDB database. That means we need to create/get the database. I’m not very familiar with MongoDB, so I looked at their site and examples to see what I could do with it.
I know that I don’t really want to wait for database transactions in my unit test, so I’m planning on mocking them (I’ll test the actual transactions in my system test). So I probably need to be able to get and set a Mongo DB.
So I’ll be working in two new files, mongodb.py and test_mongodb.py.
12def test_get_db_before_create_raises_exception(self):self.assertRaises(RuntimeError, MongoDB.get_mongodb)
If I try to get the Mongo database before I set it, I want to raise an exception.
12345678class MongoDB(object):mongo = None@staticmethoddef get_mongodb():if MongoDB.mongo is None:raise RuntimeError("Did not call set_mongodb before get_mongodb")return MongoDB.mongo
Now I have a singleton mongo instance that either gets returned or raises an exception on error.
Now… my unit test container also reports code coverage. Notice here that I don’t test the return case with my test case. I could have just written code to raise RuntimeError, excluding the if statement and the return statement in the get_mongodb function. So I wrote too much source code without testing it. Yep, I’m not perfect. Oh well. I guess my dreams are shattered. 🙂 My next cycle will test that part.
- No refactoring
- Cycle 4: time to be able to set the database and then get it.
1234def test_get_db_after_create(self):fake_mongo = "abc"MongoDB.set_mongodb(fake_mongo)self.assertEqual(MongoDB.get_mongodb(), fake_mongo)
Oh yeah, that’s right. My database is a string!!! Haha, not really. But for testing purposes, this will suffice. I just need to make sure that whatever gets set also gets returned.
123@staticmethoddef set_mongodb(mongodb):MongoDB.mongo = mongodb
Now that the set function is there, the test passes and I get back to 100% code coverage.
- Singletons don’t play very nicely with testing. Because one test case can set the singleton and do things to that singleton that may affect the next test case. For that reason, I refactor my code to have a fresh start with the singleton every time in the test case. I do this by adding a teardown function to my MongoDB test class.
Now I’ll have to start with a fresh DB every time I run a test case.
I’m not going to keep writing these stages over and over. I think by now you probably git the gist of it. I did mention that I didn’t test actual database operations (I’ll test that with a system test that I’ll write about in a future post). Here’s how I mocked out the database transactions (in a separate flask_tester.py file):
URI = "/test"
def __init__(self, resource):
app = Flask(__name__)
app.testing = True
self.test_client = app.test_client()
api = Api(app)
self.mongo_patch = mock.patch(resource.__module__ + ".MongoDB")
def post(self, data):
self.mock_db = self.mock_mongo = self.mongo_patch.start()
Anytime I use the MongoDB, it will replace it with MagicMock and I’ll get a self.mock_db variable instead. In addition, a get and post can be used to perform those actions to flask’s built-in test client. Everything uses the “/test” URI. Now to see how this is used in an actual test…
self.tester = TestResource(Person)
With the Person code, I pass in the Person class as the resource (which the TestResource maps to the “/test” URI) and call the start mocking function. I call the stop mocking function in the teardown. At the time of this writing, I haven’t developed my get and post mocks as well as I’d like, but the idea would be…
- In the mock DB, make the find function not find something.
- Then make sure a post works.
- Make sure a get on everything returns nothing.
- A get on a specific index should fail.
- Make the mock DB find function return something.
- The post should fail (since you are inserting to something that already exists).
- The get function on the /test URI should return one item with its index (the person’s name).
- The get function on the test URI with the name should return the item without its index (the person’s name).
After several cycles of TDD, I had a very simple REST interface ready to go (I continued getting the Person object with just a very basic GET and POST function working – I chose not to elaborate on that in this post). Unfortunately, my unit tests do not test the actual insertion and deletion into the Mongo database. That has been mocked out. So I could be missing some bugs in how I’m using that library!
That’s when I started manual testing and realizing it was time for some automated system testing. We will look into pyresttest next time and how I used it to test the entire application as a black box test.