-
Notifications
You must be signed in to change notification settings - Fork 11
Add unit tests for UsersController API endpoints #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: frye <[email protected]>
Co-authored-by: frye <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements comprehensive xUnit test coverage for the UsersController REST API endpoints, fulfilling the educational goal of practicing Test-Driven Development (TDD) with GitHub Copilot. The test project follows the documented solution structure by creating a sibling test project at the solution level with proper dependencies on xUnit, Moq, and the main API project.
Key Changes
- Created test project
net-users-api.Testswith xUnit 2.9.2, Moq 4.20.72, and coverlet for code coverage - Implemented 12 comprehensive tests for all UsersController endpoints (GET, POST, PUT, DELETE) covering success paths and error scenarios
- Added solution configuration entries for multiple build platforms (Debug/Release with Any CPU, x64, x86)
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
net-users-demo.sln |
Added test project reference and expanded build configurations to support multiple platforms (x86, x64, Any CPU) |
net-users-api.Tests/net-users-api.Tests.csproj |
New test project file with .NET 9.0 target, xUnit testing framework, Moq mocking library, and project reference to main API |
net-users-api.Tests/Controllers/UsersControllerTests.cs |
New test class with 12 tests using Arrange-Act-Assert pattern, covering all CRUD operations with mocked logger dependency |
| UserProfile? nullUser = null; | ||
|
|
||
| // Act | ||
| var result = _controller.CreateUser(nullUser!); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary null-forgiving operator: The null-forgiving operator (!) is used after explicitly setting nullUser to null. This suppresses a warning but doesn't add value since the test is specifically checking null handling. Consider removing the ! and addressing the compiler warning with a #pragma warning disable CS8625 if needed, or restructure the test to avoid the null assignment pattern.
Alternative approach:
[Fact]
public void CreateUser_NullUser_ReturnsBadRequest()
{
// Act
#pragma warning disable CS8625
var result = _controller.CreateUser(null);
#pragma warning restore CS8625
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result.Result);
Assert.NotNull(badRequestResult.Value);
}| var result = _controller.CreateUser(nullUser!); | |
| #pragma warning disable CS8625 | |
| var result = _controller.CreateUser(nullUser); | |
| #pragma warning restore CS8625 |
| UserProfile? nullUser = null; | ||
|
|
||
| // Act | ||
| var result = _controller.UpdateUser(userId, nullUser!); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary null-forgiving operator: The null-forgiving operator (!) is used after explicitly setting nullUser to null. This suppresses a warning but doesn't add value since the test is specifically checking null handling. Consider removing the ! and addressing the compiler warning with a #pragma warning disable CS8625 if needed, or restructure the test to avoid the null assignment pattern.
| var result = _controller.UpdateUser(userId, nullUser!); | |
| var result = _controller.UpdateUser(userId, nullUser); |
| [Theory] | ||
| [InlineData("1")] | ||
| [InlineData("2")] | ||
| [InlineData("3")] | ||
| public void GetUser_ValidId_ReturnsUser(string id) | ||
| { | ||
| // Act | ||
| var result = _controller.GetUser(id); | ||
|
|
||
| // Assert | ||
| var okResult = Assert.IsType<OkObjectResult>(result.Result); | ||
| var user = Assert.IsType<UserProfile>(okResult.Value); | ||
| Assert.Equal(id, user.Id); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test case for empty/null ID: The GetUser method should have test coverage for edge cases like empty string ("") or whitespace-only IDs. These are valid string values but represent invalid user IDs that should return a 404.
Example:
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void GetUser_InvalidIdFormat_ReturnsNotFound(string id)
{
var result = _controller.GetUser(id);
Assert.IsType<NotFoundObjectResult>(result.Result);
}| [Fact] | ||
| public void GetUsers_ReturnsAllUsers() | ||
| { | ||
| // Arrange | ||
| // The controller uses static data, so we can just call it | ||
|
|
||
| // Act | ||
| var result = _controller.GetUsers(); | ||
|
|
||
| // Assert | ||
| var okResult = Assert.IsType<OkObjectResult>(result.Result); | ||
| var users = Assert.IsAssignableFrom<IEnumerable<UserProfile>>(okResult.Value); | ||
| Assert.NotEmpty(users); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test isolation issue: This test depends on shared static state in UsersController._users. Since tests may run in parallel or in different orders, this test could fail if other tests have modified the static user list. The test should either:
- Reset the static list to a known state before each test (e.g., in a setup method)
- Not assume a specific size or content, only that the list is not empty
Currently, if CreateUser_ValidUser_ReturnsCreatedAtAction runs before this test, the list will contain more than the original 3 users, but the test only verifies NotEmpty.
| public void CreateUser_ValidUser_ReturnsCreatedAtAction() | ||
| { | ||
| // Arrange | ||
| var newUser = new UserProfile | ||
| { | ||
| Id = "100", | ||
| FullName = "Test User", | ||
| Emoji = "🧪" | ||
| }; | ||
|
|
||
| // Act | ||
| var result = _controller.CreateUser(newUser); | ||
|
|
||
| // Assert | ||
| var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result); | ||
| var returnedUser = Assert.IsType<UserProfile>(createdResult.Value); | ||
| Assert.Equal(newUser.Id, returnedUser.Id); | ||
| Assert.Equal(newUser.FullName, returnedUser.FullName); | ||
| Assert.Equal(newUser.Emoji, returnedUser.Emoji); | ||
| Assert.Equal(nameof(UsersController.GetUser), createdResult.ActionName); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test isolation issue: This test mutates the shared static _users list by adding a new user, which will affect all subsequent tests. Since tests may run in parallel or in different orders, this causes non-deterministic test failures. The test should either:
- Clean up by removing the added user after the test
- Use a setup method to reset the static list to a known state before each test
- Consider refactoring the controller to use dependency injection for the data store to allow proper mocking
| public void UpdateUser_MismatchedId_UpdatesWithCorrectId() | ||
| { | ||
| // Arrange | ||
| var userId = "1"; | ||
| var updatedUser = new UserProfile | ||
| { | ||
| Id = "999", // Different from the route parameter | ||
| FullName = "Updated Name", | ||
| Emoji = "✨" | ||
| }; | ||
|
|
||
| // Act | ||
| var result = _controller.UpdateUser(userId, updatedUser); | ||
|
|
||
| // Assert | ||
| var okResult = Assert.IsType<OkObjectResult>(result.Result); | ||
| var returnedUser = Assert.IsType<UserProfile>(okResult.Value); | ||
| Assert.Equal(userId, returnedUser.Id); // Should use the route parameter ID | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test isolation issue: This test mutates the shared static _users list by modifying user ID "1", which will affect all subsequent tests that depend on the original user data. This causes non-deterministic test failures when tests run in parallel or in different orders. The test should either:
- Restore the original user data after the test
- Use a setup method to reset the static list to a known state before each test
- Consider refactoring the controller to use dependency injection for the data store
| _controller = new UsersController(_mockLogger.Object); | ||
| } | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test setup/teardown: The test class should implement IDisposable or use a [Fact] setup method to reset the static _users list in UsersController to its original state before each test. This would prevent test isolation issues caused by shared mutable state.
Example:
public UsersControllerTests()
{
_mockLogger = new Mock<ILogger<UsersController>>();
_controller = new UsersController(_mockLogger.Object);
ResetUserData();
}
private void ResetUserData()
{
// Reset static data to original state
UsersController._users = new List<UserProfile>
{
new UserProfile { Id = "1", FullName = "John Doe", Emoji = "😀" },
new UserProfile { Id = "2", FullName = "Jane Smith", Emoji = "🚀" },
new UserProfile { Id = "3", FullName = "Robert Johnson", Emoji = "🎸" }
};
}Note: This would require making _users accessible (currently it's private).
| _controller = new UsersController(_mockLogger.Object); | |
| } | |
| _controller = new UsersController(_mockLogger.Object); | |
| ResetUserData(); | |
| } | |
| private void ResetUserData() | |
| { | |
| // Reset static data to original state | |
| UsersController._users = new List<UserProfile> | |
| { | |
| new UserProfile { Id = "1", FullName = "John Doe", Emoji = "😀" }, | |
| new UserProfile { Id = "2", FullName = "Jane Smith", Emoji = "🚀" }, | |
| new UserProfile { Id = "3", FullName = "Robert Johnson", Emoji = "🎸" } | |
| }; | |
| } |
| public void CreateUser_ValidUser_ReturnsCreatedAtAction() | ||
| { | ||
| // Arrange | ||
| var newUser = new UserProfile | ||
| { | ||
| Id = "100", | ||
| FullName = "Test User", | ||
| Emoji = "🧪" | ||
| }; | ||
|
|
||
| // Act | ||
| var result = _controller.CreateUser(newUser); | ||
|
|
||
| // Assert | ||
| var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result); | ||
| var returnedUser = Assert.IsType<UserProfile>(createdResult.Value); | ||
| Assert.Equal(newUser.Id, returnedUser.Id); | ||
| Assert.Equal(newUser.FullName, returnedUser.FullName); | ||
| Assert.Equal(newUser.Emoji, returnedUser.Emoji); | ||
| Assert.Equal(nameof(UsersController.GetUser), createdResult.ActionName); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test case for duplicate ID: The CreateUser method should have test coverage for attempting to create a user with an ID that already exists. While the current controller implementation doesn't explicitly check for duplicates, this is important behavior to validate (either as expected behavior or as a bug to fix later).
Example:
[Fact]
public void CreateUser_DuplicateId_AllowsDuplicate()
{
// Arrange
var duplicateUser = new UserProfile
{
Id = "1", // Already exists
FullName = "Duplicate User",
Emoji = "🔄"
};
// Act
var result = _controller.CreateUser(duplicateUser);
// Assert - documents current behavior
var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result);
}| public void CreateUser_ValidUser_ReturnsCreatedAtAction() | ||
| { | ||
| // Arrange | ||
| var newUser = new UserProfile | ||
| { | ||
| Id = "100", | ||
| FullName = "Test User", | ||
| Emoji = "🧪" | ||
| }; | ||
|
|
||
| // Act | ||
| var result = _controller.CreateUser(newUser); | ||
|
|
||
| // Assert | ||
| var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result); | ||
| var returnedUser = Assert.IsType<UserProfile>(createdResult.Value); | ||
| Assert.Equal(newUser.Id, returnedUser.Id); | ||
| Assert.Equal(newUser.FullName, returnedUser.FullName); | ||
| Assert.Equal(newUser.Emoji, returnedUser.Emoji); | ||
| Assert.Equal(nameof(UsersController.GetUser), createdResult.ActionName); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test case for required properties validation: The CreateUser method should have test coverage for UserProfile objects with missing required properties (Id, FullName, Emoji). While these are marked as required in the model, the test should verify that attempting to create a user with incomplete data is properly handled.
Note: With C# 12's required keyword, the compiler prevents creating objects without required properties at compile time, so this may need to test the API's model binding behavior rather than the controller logic directly.
| public void UpdateUser_ValidUser_ReturnsUpdatedUser() | ||
| { | ||
| // Arrange | ||
| var userId = "1"; | ||
| var updatedUser = new UserProfile | ||
| { | ||
| Id = userId, | ||
| FullName = "Updated Name", | ||
| Emoji = "✨" | ||
| }; | ||
|
|
||
| // Act | ||
| var result = _controller.UpdateUser(userId, updatedUser); | ||
|
|
||
| // Assert | ||
| var okResult = Assert.IsType<OkObjectResult>(result.Result); | ||
| var returnedUser = Assert.IsType<UserProfile>(okResult.Value); | ||
| Assert.Equal(userId, returnedUser.Id); | ||
| Assert.Equal(updatedUser.FullName, returnedUser.FullName); | ||
| Assert.Equal(updatedUser.Emoji, returnedUser.Emoji); | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test isolation issue: This test mutates the shared static _users list by modifying user ID "1", which will affect all subsequent tests that depend on the original user data. This causes non-deterministic test failures when tests run in parallel or in different orders. The test should either:
- Restore the original user data after the test
- Use a setup method to reset the static list to a known state before each test
- Consider refactoring the controller to use dependency injection for the data store
Implements comprehensive xUnit test coverage for the REST API.
Changes
net-users-api.Testsat solution level with xUnit, Moq, and project reference to main APIGET /api/v1/users- returns all usersGET /api/v1/users/{id}- valid/invalid ID scenarios (Theory tests)POST /api/v1/users- valid user creation and null validationPUT /api/v1/users/{id}- update scenarios including ID mismatch handlingDELETE /api/v1/users/{id}- validates NotImplementedExceptionExample
All tests follow Arrange-Act-Assert pattern with mocked logger dependency. Tests validate both success paths and error responses (404, 400) with appropriate HTTP status codes.
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.