Lesson 3
Testing CRUD Operations with Setup and Teardown
Testing CRUD Operations with Setup and Teardown

Welcome to another step in our journey to mastering automated API testing with Python. So far, you've learned how to organize tests using pytest classes and fixtures. Today, we will focus on automating tests for CRUD operations — Create, Read, Update, and Delete — which are integral actions for managing data in any application that uses RESTful APIs. Automated testing of these operations is essential to ensure that APIs function correctly and modify resources as expected. Thorough testing of CRUD operations will help you catch issues early and ensure API reliability.

Fixtures and Their Role in Setup and Teardown

In automated testing, setup and teardown are fundamental concepts that help ensure each test has a clean start and finish. Setup is about preparing what you need before a test runs, like creating test data or setting up configurations. Teardown is cleaning up afterward, removing any leftovers from the test, such as deleting test data, so future tests aren't affected. This process makes sure tests don't interfere with each other.

We'll use pytest fixtures to make setup and teardown automatic. Imagine testing a todo API with CRUD operations:

  • Setup Phase: Runs before every test to create a fresh todo item, providing each test with the necessary starting data.
  • Teardown Phase: Runs after every test to delete the todo item, ensuring no leftover data impacts subsequent tests.

Fixtures handle these phases automatically, before and after every test, keeping your tests independent and the environment clean.

Implementing Setup and Teardown with Pytest Fixtures

To effectively manage setup and teardown for CRUD operations, we utilize pytest fixtures. These fixtures automate the process, ensuring each test starts with the right conditions and ends without leaving any trace. By using the autouse=True parameter, the fixture runs automatically before and after each test in the class, so you don't need to invoke it manually in every test. This means every test begins and ends with a clean state, which is crucial to avoid any interference between tests.

Here's an example of how the fixture is structured:

Python
1import pytest 2import requests 3 4BASE_URL = "http://localhost:8000" 5 6class TestCRUDOperations: 7 8 @pytest.fixture(autouse=True) 9 def setup_and_teardown(self): 10 # Setup Actions: Prepare a new todo 11 self.todo_data = { 12 "title": "Setup Todo", 13 "description": "For testing CRUD operations", 14 "done": False 15 } 16 create_response = requests.post(f"{BASE_URL}/todos", json=self.todo_data) 17 if create_response.status_code != 201: 18 raise Exception("Failed to create a todo for setup") 19 self.todo_id = create_response.json().get('id') 20 21 yield # Execute test 22 23 # Teardown Actions: Clean up by deleting the todo 24 if self.todo_id: 25 requests.delete(f"{BASE_URL}/todos/{self.todo_id}")

Within this fixture, the setup actions occur before each test to create a todo item, ensuring the test environment has the necessary data. The yield statement separates setup from teardown, signifying where the test itself runs. After each test, the teardown actions delete the created todo, maintaining a clean slate for subsequent tests. This ensures consistent, independent test runs free from interference caused by leftover data.

Defining CRUD Tests within the Class

With setup and teardown processes in place using pytest fixtures, we establish a consistent environment for our tests. Now, we'll define the specific tests for each CRUD operation within the TestCRUDOperations class. These tests will leverage the setup todo item, ensuring that we assess the ability to create, read, update, and delete resources accurately. Let's explore each operation through dedicated test functions.

Testing Read Operation

In our CRUD operations, the Read test checks if we can successfully retrieve a todo item. Using the requests.get method, we fetch the todo item created during the setup. The test verifies the HTTP status code is 200, indicating success, and it asserts that the data returned matches our expectations. This confirms that our API's read functionality works correctly.

Python
1def test_read_todo(self): 2 # Act 3 response = requests.get(f"{BASE_URL}/todos/{self.todo_id}") 4 5 # Assert 6 assert response.status_code == 200 7 fetched_todo = response.json() 8 assert fetched_todo['title'] == self.todo_data['title'] 9 assert fetched_todo['description'] == self.todo_data['description'] 10 assert fetched_todo['done'] == self.todo_data['done']
Testing Update Operation with PATCH

The Update operation using PATCH focuses on modifying specific fields of a todo item. Here, we send a PATCH request to change the done status to True. The test checks if the status code is 200, confirming the update was successful. We also verify the field was accurately updated in the API. This ensures that partial updates with PATCH are functioning as intended.

Python
1def test_update_todo_with_patch(self): 2 # Arrange 3 update_data = {"done": True} 4 5 # Act 6 response = requests.patch(f"{BASE_URL}/todos/{self.todo_id}", json=update_data) 7 8 # Assert 9 assert response.status_code == 200 10 updated_todo = response.json() 11 assert updated_todo['done'] is True
Testing Update Operation with PUT

For a complete replacement of fields, the Update operation using PUT is employed. This test sends a PUT request to replace all fields of the todo item with new data. We assert the status code is 200, indicating the operation succeeded, and confirm that all fields match the updated values. This test validates that full updates with PUT are correctly processed by the API.

Python
1def test_update_todo_with_put(self): 2 # Arrange 3 put_data = { 4 "title": "Updated Title", 5 "description": "Updated Description", 6 "done": True 7 } 8 9 # Act 10 response = requests.put(f"{BASE_URL}/todos/{self.todo_id}", json=put_data) 11 12 # Assert 13 assert response.status_code == 200 14 updated_todo = response.json() 15 assert updated_todo['title'] == put_data['title'] 16 assert updated_todo['description'] == put_data['description'] 17 assert updated_todo['done'] == put_data['done']
Testing Delete Operation with DELETE

The Delete test checks if a todo item can be removed successfully. A DELETE request is sent to the API, and the test verifies the status code is 204, signifying a successful deletion with no content returned. To confirm the deletion, we attempt to retrieve the same todo item and expect a 404 status code, indicating it no longer exists. This ensures the API's delete functionality behaves as expected.

Python
1def test_delete_todo(self): 2 # Act 3 response = requests.delete(f"{BASE_URL}/todos/{self.todo_id}") 4 5 # Assert 6 assert response.status_code == 204 7 get_deleted_response = requests.get(f"{BASE_URL}/todos/{self.todo_id}") 8 assert get_deleted_response.status_code == 404

These structured tests demonstrate the essential steps of the Arrange-Act-Assert pattern, ensuring each CRUD operation is verified thoroughly, with setup and teardown maintaining a consistent testing environment.

Summary and Practice Preparation

In today's lesson, we've delved into the essentials of testing CRUD operations with setup and teardown using pytest fixtures. You've seen how automating these processes helps maintain a structured and reliable testing environment. Now, it's your turn to practice these concepts with hands-on exercises that will reinforce your understanding and confidence in applying these techniques. As you continue to build your skills, you'll be better equipped to ensure API robustness and reliability. Keep up the great work, and prepare to explore testing authenticated endpoints moving forward!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.