Welcome to another step in our journey to mastering automated API testing with Dart. Today, we'll focus on testing CRUD operations — Create, Read, Update, and Delete — which form the backbone of RESTful API interactions. We'll see how setup and teardown functions can streamline our testing process and ensure reliable, isolated tests.
When testing CRUD operations, we need a consistent environment for each test. This is where setup and teardown functions shine. Let's structure our test file:
Dart1import 'dart:convert'; 2import 'package:http/http.dart' as http; 3import 'package:test/test.dart'; 4 5void main() { 6 final String baseUrl = "http://localhost:8000"; 7 late int todoId; 8 9 setUp(() async { 10 // Setup code: Create a todo item for testing 11 final response = await http.post( 12 Uri.parse('$baseUrl/todos'), 13 headers: {"Content-Type": "application/json"}, 14 body: jsonEncode({ 15 "title": "Setup Todo", 16 "description": "For testing CRUD operations", 17 "done": false 18 }), 19 ); 20 if (response.statusCode != 201) { 21 throw Exception("Failed to create a todo for setup"); 22 } 23 24 // Store the ID of the created todo 25 final createdTodo = jsonDecode(response.body); 26 todoId = createdTodo['id']; 27 }); 28 29 tearDown(() async { 30 // Teardown code: Clean up the created todo item 31 await http.delete(Uri.parse('$baseUrl/todos/$todoId')); 32 });
Our setUp
function creates a fresh todo item before each test, while tearDown
removes it afterward. This ensures each test operates on a clean slate.
You might notice we don't have a dedicated test for the Create operation. This is intentional and efficient:
- We're already testing creation in our
setUp
function - If creation fails, the
setUp
function throws an exception - This would cause all tests to fail, clearly indicating a problem with the Create operation
This approach avoids redundancy while still ensuring the Create functionality is thoroughly verified. If the API's Create endpoint has issues, we'll know immediately because no tests will be able to run.
With our todo item created in setup, we can now test the Read operation:
Dart1test('Read a todo', () async { 2 // Act 3 final response = await http.get(Uri.parse('$baseUrl/todos/$todoId')); 4 5 // Assert 6 expect(response.statusCode, equals(200)); 7 final fetchedTodo = jsonDecode(response.body); 8 expect(fetchedTodo['title'], equals("Setup Todo")); 9 expect(fetchedTodo['description'], equals("For testing CRUD operations")); 10 expect(fetchedTodo['done'], isFalse); 11});
This test verifies we can retrieve the todo item and that its properties match our expectations.
The PATCH method is designed for partial updates, allowing clients to modify specific fields without sending the entire resource.
In our test, we'll focus on updating just the done
status of our todo item. We'll first prepare the update data, then send the PATCH request, and finally verify that only the specified field was changed while the rest remained intact.
Dart1test('Update a todo with PATCH', () async { 2 // Arrange 3 final updateData = {"done": true}; 4 5 // Act 6 final response = await http.patch( 7 Uri.parse('$baseUrl/todos/$todoId'), 8 headers: {"Content-Type": "application/json"}, 9 body: jsonEncode(updateData), 10 ); 11 12 // Assert 13 expect(response.statusCode, equals(200)); 14 final updatedTodo = jsonDecode(response.body); 15 expect(updatedTodo['done'], isTrue); 16});
This test focuses on updating just one field, which is the primary use case for PATCH.
Unlike PATCH, the PUT method is used for complete replacement of a resource. In our test, we'll replace all fields of the todo item with new values. We'll then verify that the entire resource has been updated to match our new data. This ensures that our API correctly implements the PUT semantics according to RESTful principles.
Dart1test('Update a todo with PUT', () async { 2 // Arrange 3 final putData = { 4 "title": "Updated Title", 5 "description": "Updated Description", 6 "done": true 7 }; 8 9 // Act 10 final response = await http.put( 11 Uri.parse('$baseUrl/todos/$todoId'), 12 headers: {"Content-Type": "application/json"}, 13 body: jsonEncode(putData), 14 ); 15 16 // Assert 17 expect(response.statusCode, equals(200)); 18 final updatedTodo = jsonDecode(response.body); 19 expect(updatedTodo['title'], equals(putData['title'])); 20 expect(updatedTodo['description'], equals(putData['description'])); 21 expect(updatedTodo['done'], isTrue); 22});
Here we replace the entire resource, verifying all fields are updated correctly.
Finally, let's test the Delete operation:
Dart1test('Delete a todo', () async { 2 // Act 3 final response = await http.delete(Uri.parse('$baseUrl/todos/$todoId')); 4 5 // Assert 6 expect(response.statusCode, equals(204)); 7 final getDeletedResponse = await http.get(Uri.parse('$baseUrl/todos/$todoId')); 8 expect(getDeletedResponse.statusCode, equals(404)); 9});
This test verifies that:
- The delete request returns a 204 status (success with no content)
- The resource is no longer accessible (404 Not Found)
Interestingly, after this test completes, our tearDown
function will still attempt to delete the same todo item. This redundancy is actually beneficial—it ensures cleanup happens even if a test fails before completing its own delete operation. The delete request in tearDown
will simply return a 404 status code, which doesn't affect our test results.
This structured approach to testing CRUD operations offers several advantages:
- Efficiency: We test creation implicitly in setup, avoiding redundant tests
- Isolation: Each test runs independently with fresh data
- Completeness: We cover all CRUD operations thoroughly
- Reliability: Cleanup happens automatically, even after test failures
- Clarity: Each test focuses on a single operation, making the test intent clear
By organizing our tests this way, we create a robust suite that thoroughly validates our API's functionality while remaining maintainable and efficient.
In this lesson, we've seen how to structure tests for CRUD operations using setup and teardown functions. We've learned why we don't need a separate test for creation, how to test read and update operations, and how the delete test interacts with our teardown function.
This approach ensures comprehensive testing of your API while maintaining test independence and reliability. In the upcoming practice exercises, you'll apply these concepts to reinforce your understanding and build confidence in your API testing skills.
