Now, we will write a first round of unit tests. Specifically, we will write unit tests related to the user and message category resources: UserResource
, UserListResource
, CategoryResource
, and CategoryListResource
. Create a new tests
sub-folder within the api
folder. Then, create a new test_views.py
file within the new api/tests
sub-folder. Add the following lines, that declare many import
statements and the first methods for the InitialTests
class. The code file for the sample is included in the restful_python_chapter_08_01
folder:
from app import create_app from base64 import b64encode from flask import current_app, json, url_for from models import db, Category, Message, User import status from unittest import TestCase class InitialTests(TestCase): def setUp(self): self.app = create_app('test_config') self.test_client = self.app.test_client() self.app_context = self.app.app_context() self.app_context.push() self.test_user_name = 'testuser' self.test_user_password = 'T3s!p4s5w0RDd12#' db.create_all() def tearDown(self): db.session.remove() db.drop_all() self.app_context.pop() def get_accept_content_type_headers(self): return { 'Accept': 'application/json', 'Content-Type': 'application/json' } def get_authentication_headers(self, username, password): authentication_headers = self.get_accept_content_type_headers() authentication_headers['Authorization'] = 'Basic ' + b64encode((username + ':' + password).encode('utf- 8')).decode('utf-8') return authentication_headers
The InitialTests
class is a subclass of unittest.TestCase
. The class overrides the setUp
method that will be executed before each test method runs. The method calls the create_app
function, declared in the app
module, with 'test_config'
as an argument. The function will set up a Flask app with this module as the configuration file, and therefore, the app will use the previously created configuration file that specifies the desired values for our testing database and environment. Then, the code sets the testing attribute for the recently created app
to True
in order for the exception to propagate to the test client.
The next line calls the self.app.test_client
method to create a test client for the previously created Flask application and saves the test client in the test_client
attribute. We will use the test client in our test methods to easily compose and send requests to our API. Then, the code saves and pushes the application context and creates two attributes with the user name and password we will use for our tests. Finally, the method calls the db.create_all
method to create all the necessary tables in our test database configured in the test_config.py
file.
The InitialTests
class overrides the tearDown
method that will be executed after each test method runs. The code removes the SQLAlchemy session, drops all the tables that we created in the test database before starting the execution of the tests, and pops the application context. This way, after each test finishes its execution, the test database will be empty again.
The get_accept_content_type_headers
method builds and returns a dictionary (dict
) with the values of the Accept
and Content-Type
header keys set to 'application/json'
. We will call this method in our tests whenever we have to build a header to compose our requests without authentication.
The get_authentication_headers
method calls the previously explained get_accept_content_type_headers
method to generate the header key-value pairs without authentication. Then, the code adds the necessary value to the Authorization
key with the appropriate encoding to provide the user name and password received in the username
and password
arguments. The last line returns the generated dictionary that includes authentication information. We will call this method in our tests whenever we have to build a header to compose our requests with authentication. We will use the user name and password we stored in attributes the setUp
method.
Open the previously created test_views.py
file within the new api/tests
sub-folder. Add the following lines that declare many methods for the InitialTests
class. The code file for the sample is included in the restful_python_chapter_08_01
folder.
def test_request_without_authentication(self): """ Ensure we cannot access a resource that requirest authentication without an appropriate authentication header """ response = self.test_client.get( url_for('api.messagelistresource', _external=True), headers=self.get_accept_content_type_headers()) self.assertTrue(response.status_code == status.HTTP_401_UNAUTHORIZED) def create_user(self, name, password): url = url_for('api.userlistresource', _external=True) data = {'name': name, 'password': password} response = self.test_client.post( url, headers=self.get_accept_content_type_headers(), data=json.dumps(data)) return response def create_category(self, name): url = url_for('api.categorylistresource', _external=True) data = {'name': name} response = self.test_client.post( url, headers=self.get_authentication_headers(self.test_user_name, self.test_user_password), data=json.dumps(data)) return response def test_create_and_retrieve_category(self): """ Ensure we can create a new Category and then retrieve it """ create_user_response = self.create_user(self.test_user_name, self.test_user_password) self.assertEqual(create_user_response.status_code, status.HTTP_201_CREATED) new_category_name = 'New Information' post_response = self.create_category(new_category_name) self.assertEqual(post_response.status_code, status.HTTP_201_CREATED) self.assertEqual(Category.query.count(), 1) post_response_data = json.loads(post_response.get_data(as_text=True)) self.assertEqual(post_response_data['name'], new_category_name) new_category_url = post_response_data['url'] get_response = self.test_client.get( new_category_url, headers=self.get_authentication_headers(self.test_user_name, self.test_user_password)) get_response_data = json.loads(get_response.get_data(as_text=True)) self.assertEqual(get_response.status_code, status.HTTP_200_OK) self.assertEqual(get_response_data['name'], new_category_name)
The test_request_without_authentication
method tests whether we have been rejected access to a resource that requires authentication when we don't provide an appropriate authentication header with the request. The method uses the test client to compose and send an HTTP GET
request to the URL generated for the 'api.messagelistresource'
resource to retrieve the list of messages. We need an authenticated request to retrieve the list of messages. However, the code calls the get_authentication_headers
method to set the value for the headers argument in the call to self.test_client.get
, and therefore, the code generates a request without authentication. Finally, the method uses assertTrue
to check that the status_code
for the response is HTTP 401 Unauthorized (status.HTTP_401_UNAUTHORIZED
).
The create_user
method uses the test client to compose and send an HTTP POST
request to the URL generated for the 'api.userlistresource'
resource to create a new user with the name and password received as arguments. We don't need an authenticated request to create a new user, and therefore, the code calls the previously explained get_accept_content_type_headers
method to set the value for the headers argument in the call to self.test_client.post
. Finally, the code returns the response from the POST
request. Whenever we have to create an authenticated request, we will call the create_user
method to create a new user.
The create_category
method uses the test client to compose and send an HTTP POST
request to the URL generated for the 'api.categorylistresource'
resource to create a new Category
with the name received as an argument. We need an authenticated request to create a new Category
, and therefore, the code calls the previously explained get_authentication_headers
method to set the value for the headers argument in the call to self.test_client.post
. The user name and password are set to self.test_user_name
and self.test_user_password
. Finally, the code returns the response from the POST
request. Whenever we have to create a category, we will call the create_category
method after the appropriate user that authenticates the request has been created.
The test_create_and_retrieve_category
method tests whether we can create a new Category
and then retrieve it. The method calls the previously explained create_user
method to create a new user and then use it to authenticate the HTTP POST
request generated in the create_game_category
method. Then, the code composes and sends an HTTP GET
method to retrieve the recently created Category with the URL received in the response of the previous HTTP POST
request. The method uses assertEqual
to check for the following expected results:
status_code
for the HTTP POST
response is HTTP 201 Created (status.HTTP_201_CREATED
)Category
objects retrieved from the database is 1
status_code
for the HTTP GET
response is HTTP 200 OK (status.HTTP_200_OK
)name
key in the HTTP GET
response is equal to the name specified for the new categoryOpen the previously created test_views.py
file within the new api/tests
sub-folder. Add the following lines that declare many methods for the InitialTests
class. The code file for the sample is included in the restful_python_chapter_08_01
folder.
def test_create_duplicated_category(self): """ Ensure we cannot create a duplicated Category """ create_user_response = self.create_user(self.test_user_name, self.test_user_password) self.assertEqual(create_user_response.status_code, status.HTTP_201_CREATED) new_category_name = 'New Information' post_response = self.create_category(new_category_name) self.assertEqual(post_response.status_code, status.HTTP_201_CREATED) self.assertEqual(Category.query.count(), 1) post_response_data = json.loads(post_response.get_data(as_text=True)) self.assertEqual(post_response_data['name'], new_category_name) second_post_response = self.create_category(new_category_name) self.assertEqual(second_post_response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Category.query.count(), 1) def test_retrieve_categories_list(self): """ Ensure we can retrieve the categories list """ create_user_response = self.create_user(self.test_user_name, self.test_user_password) self.assertEqual(create_user_response.status_code, status.HTTP_201_CREATED) new_category_name_1 = 'Error' post_response_1 = self.create_category(new_category_name_1) self.assertEqual(post_response_1.status_code, status.HTTP_201_CREATED) new_category_name_2 = 'Warning' post_response_2 = self.create_category(new_category_name_2) self.assertEqual(post_response_2.status_code, status.HTTP_201_CREATED) url = url_for('api.categorylistresource', _external=True) get_response = self.test_client.get( url, headers=self.get_authentication_headers(self.test_user_name, self.test_user_password)) get_response_data = json.loads(get_response.get_data(as_text=True)) self.assertEqual(get_response.status_code, status.HTTP_200_OK) self.assertEqual(len(get_response_data), 2) self.assertEqual(get_response_data[0]['name'], new_category_name_1) self.assertEqual(get_response_data[1]['name'], new_category_name_2) """ Ensure we can update the name for an existing category """ create_user_response = self.create_user(self.test_user_name, self.test_user_password) self.assertEqual(create_user_response.status_code, status.HTTP_201_CREATED) new_category_name_1 = 'Error 1' post_response_1 = self.create_category(new_category_name_1) self.assertEqual(post_response_1.status_code, status.HTTP_201_CREATED) post_response_data_1 = json.loads(post_response_1.get_data(as_text=True)) new_category_url = post_response_data_1['url'] new_category_name_2 = 'Error 2' data = {'name': new_category_name_2} patch_response = self.test_client.patch( new_category_url, headers=self.get_authentication_headers(self.test_user_name, self.test_user_password), data=json.dumps(data)) self.assertEqual(patch_response.status_code, status.HTTP_200_OK) get_response = self.test_client.get( new_category_url, headers=self.get_authentication_headers(self.test_user_name, self.test_user_password)) get_response_data = json.loads(get_response.get_data(as_text=True)) self.assertEqual(get_response.status_code, status.HTTP_200_OK) self.assertEqual(get_response_data['name'], new_category_name_2)
The class declares the following methods whose name start with the test_
prefix:
test_create_duplicated_ category
: Tests whether the unique constraints don't make it possible for us to create two categories with the same name or not. The second time we compose and send an HTTP POST
request with a duplicate category name, we must receive an HTTP 400 Bad Request status code (status.HTTP_400_BAD_REQUEST
) and the total number of Category
objects retrieved from the database must be 1
.test_retrieve_categories_list
: Tests whether we can retrieve the categories list or not. First, the method creates two categories and then it makes sure that the retrieved list includes the two created categories.test_update_game_category
: Tests whether we can update a single field for a category, specifically, its name field. The code makes sure that the name has been updated.Note that each test that requires a specific condition in the database must execute all the necessary code for the database to be in this specific condition. For example, in order to update an existing category, first we must create a new category and then we can update it. Each test method will be executed without data from the previously executed test methods in the database, that is, each test will run with a database cleaned of data from previous tests.
3.140.197.10