Skip to content

Conversation

Copy link

Copilot AI commented Dec 10, 2025

Implements comprehensive xUnit test coverage for the REST API.

Changes

  • Test project: Created net-users-api.Tests at solution level with xUnit, Moq, and project reference to main API
  • UsersController tests: 12 tests covering all implemented endpoints:
    • GET /api/v1/users - returns all users
    • GET /api/v1/users/{id} - valid/invalid ID scenarios (Theory tests)
    • POST /api/v1/users - valid user creation and null validation
    • PUT /api/v1/users/{id} - update scenarios including ID mismatch handling
    • DELETE /api/v1/users/{id} - validates NotImplementedException

Example

[Theory]
[InlineData("1")]
[InlineData("2")]
[InlineData("3")]
public void GetUser_ValidId_ReturnsUser(string id)
{
    var result = _controller.GetUser(id);
    
    var okResult = Assert.IsType<OkObjectResult>(result.Result);
    var user = Assert.IsType<UserProfile>(okResult.Value);
    Assert.Equal(id, user.Id);
}

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

Implement unit tests for me


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Add unit tests for application functionality Add unit tests for UsersController API endpoints Dec 10, 2025
Copilot AI requested a review from frye December 10, 2025 17:46
@frye frye requested a review from Copilot December 10, 2025 18:00
Copy link

Copilot AI left a 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.Tests with 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!);
Copy link

Copilot AI Dec 10, 2025

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);
}
Suggested change
var result = _controller.CreateUser(nullUser!);
#pragma warning disable CS8625
var result = _controller.CreateUser(nullUser);
#pragma warning restore CS8625

Copilot uses AI. Check for mistakes.
UserProfile? nullUser = null;

// Act
var result = _controller.UpdateUser(userId, nullUser!);
Copy link

Copilot AI Dec 10, 2025

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.

Suggested change
var result = _controller.UpdateUser(userId, nullUser!);
var result = _controller.UpdateUser(userId, nullUser);

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +48
[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);
}
Copy link

Copilot AI Dec 10, 2025

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);
}

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +33
[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);
}
Copy link

Copilot AI Dec 10, 2025

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:

  1. Reset the static list to a known state before each test (e.g., in a setup method)
  2. 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.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +85
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);
}
Copy link

Copilot AI Dec 10, 2025

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:

  1. Clean up by removing the added user after the test
  2. Use a setup method to reset the static list to a known state before each test
  3. Consider refactoring the controller to use dependency injection for the data store to allow proper mocking

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +177
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
}
Copy link

Copilot AI Dec 10, 2025

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:

  1. Restore the original user data after the test
  2. Use a setup method to reset the static list to a known state before each test
  3. Consider refactoring the controller to use dependency injection for the data store

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
_controller = new UsersController(_mockLogger.Object);
}

Copy link

Copilot AI Dec 10, 2025

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).

Suggested change
_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 = "🎸" }
};
}

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +85
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);
}
Copy link

Copilot AI Dec 10, 2025

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);
}

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +85
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);
}
Copy link

Copilot AI Dec 10, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +122
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);
}
Copy link

Copilot AI Dec 10, 2025

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:

  1. Restore the original user data after the test
  2. Use a setup method to reset the static list to a known state before each test
  3. Consider refactoring the controller to use dependency injection for the data store

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants