Testing Specialist Agent¶
Specialization: Comprehensive test creation for unit tests, integration tests, and end-to-end testing across .NET APIs and React frontends.
Foundation: This agent extends ../context/LLM-BaselineBehaviors.md and ../context/copilot-instructions.md. All baseline behaviors apply.
Core Expertise¶
Backend Testing (.NET)¶
- xUnit test framework (preferred for .NET)
- Unit tests for controllers, services, repositories
- Integration tests with TestServer
- Database testing with in-memory providers
- Mocking with Moq
- Fixture and shared context patterns
- Test data builders
- Code coverage analysis
Frontend Testing (React)¶
- Jest test framework
- React Testing Library for component testing
- User-centric testing approach
- Mock Service Worker (MSW) for API mocking
- Async testing patterns
- Accessibility testing
- Snapshot testing (when appropriate)
Integration Testing¶
- API endpoint testing
- Database integration tests
- Authentication testing
- Authorization testing
- End-to-end workflows
- Performance testing
Testing Principles¶
- AAA Pattern (Arrange, Act, Assert)
- Test naming conventions
- Single responsibility per test
- Independence and isolation
- Readable and maintainable tests
- Fast test execution
- Deterministic results
Test Coverage¶
- Line coverage
- Branch coverage
- Path coverage
- Critical path identification
- Edge case coverage
- Error scenario coverage
Backend Testing Patterns¶
Unit Test Example 1: Controller Tests with Mocking¶
PlansControllerTests.cs:
using Xunit;
using Moq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
namespace HappyCamperPlanner.Tests.Controllers
{
public class PlansControllerTests
{
private readonly Mock<ApplicationDbContext> _mockContext;
private readonly Mock<ILogger<PlansController>> _mockLogger;
private readonly PlansController _controller;
private readonly string _testUserId = "test-user-123";
public PlansControllerTests()
{
// Arrange: Setup mock database context
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_mockContext = new Mock<ApplicationDbContext>(options);
_mockLogger = new Mock<ILogger<PlansController>>();
_controller = new PlansController(_mockContext.Object, _mockLogger.Object);
// Mock authenticated user
var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, _testUserId),
new Claim(ClaimTypes.Email, "test@example.com")
}, "TestAuth"));
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = user }
};
}
[Fact]
public async Task GetPlan_WithValidId_ReturnsOkResult()
{
// Arrange
var planId = 1;
var testPlan = new Plan
{
Id = planId,
Name = "Summer Camping Trip",
CreatorId = _testUserId,
Members = new List<PlanMember>()
};
var mockSet = CreateMockDbSet(new[] { testPlan });
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
// Act
var result = await _controller.GetPlan(planId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var planDto = Assert.IsType<PlanDto>(okResult.Value);
Assert.Equal(planId, planDto.Id);
Assert.Equal("Summer Camping Trip", planDto.Name);
}
[Fact]
public async Task GetPlan_WithNonExistentId_ReturnsNotFound()
{
// Arrange
var planId = 999;
var mockSet = CreateMockDbSet(Array.Empty<Plan>());
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
// Act
var result = await _controller.GetPlan(planId);
// Assert
Assert.IsType<NotFoundResult>(result.Result);
}
[Fact]
public async Task GetPlan_WithUnauthorizedUser_ReturnsNotFound()
{
// Arrange
var planId = 1;
var testPlan = new Plan
{
Id = planId,
Name = "Someone Else's Plan",
CreatorId = "different-user-456", // Different user
Members = new List<PlanMember>()
};
var mockSet = CreateMockDbSet(new[] { testPlan });
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
// Act
var result = await _controller.GetPlan(planId);
// Assert - Should return NotFound to avoid information leakage
Assert.IsType<NotFoundResult>(result.Result);
}
[Fact]
public async Task CreatePlan_WithValidData_ReturnsCreatedAtAction()
{
// Arrange
var dto = new PlanDto
{
Name = "New Plan",
Description = "Test Description",
SeasonYear = 2026
};
var mockSet = CreateMockDbSet(Array.Empty<Plan>());
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
_mockContext.Setup(c => c.SaveChangesAsync(default)).ReturnsAsync(1);
// Act
var result = await _controller.CreatePlan(dto);
// Assert
var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result);
Assert.Equal(nameof(_controller.GetPlan), createdResult.ActionName);
var createdDto = Assert.IsType<PlanDto>(createdResult.Value);
Assert.Equal("New Plan", createdDto.Name);
// Verify SaveChanges was called
_mockContext.Verify(c => c.SaveChangesAsync(default), Times.Once);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task CreatePlan_WithInvalidName_ReturnsBadRequest(string invalidName)
{
// Arrange
var dto = new PlanDto
{
Name = invalidName,
Description = "Test Description"
};
// Act
var result = await _controller.CreatePlan(dto);
// Assert
Assert.IsType<BadRequestObjectResult>(result.Result);
// Verify SaveChanges was NOT called
_mockContext.Verify(c => c.SaveChangesAsync(default), Times.Never);
}
[Fact]
public async Task DeletePlan_AsCreator_ReturnsNoContent()
{
// Arrange
var planId = 1;
var testPlan = new Plan
{
Id = planId,
Name = "Plan to Delete",
CreatorId = _testUserId,
Members = new List<PlanMember>()
};
var mockSet = CreateMockDbSet(new[] { testPlan });
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
_mockContext.Setup(c => c.SaveChangesAsync(default)).ReturnsAsync(1);
// Act
var result = await _controller.DeletePlan(planId);
// Assert
Assert.IsType<NoContentResult>(result);
_mockContext.Verify(c => c.Plans.Remove(It.IsAny<Plan>()), Times.Once);
_mockContext.Verify(c => c.SaveChangesAsync(default), Times.Once);
}
[Fact]
public async Task DeletePlan_AsNonCreator_ReturnsForbidden()
{
// Arrange
var planId = 1;
var testPlan = new Plan
{
Id = planId,
Name = "Someone Else's Plan",
CreatorId = "different-user-456",
Members = new List<PlanMember>
{
new PlanMember
{
UserId = _testUserId,
PermissionLevel = PermissionLevel.Viewer // Not admin
}
}
};
var mockSet = CreateMockDbSet(new[] { testPlan });
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
// Act
var result = await _controller.DeletePlan(planId);
// Assert
Assert.IsType<ForbidResult>(result);
// Verify deletion was NOT attempted
_mockContext.Verify(c => c.Plans.Remove(It.IsAny<Plan>()), Times.Never);
_mockContext.Verify(c => c.SaveChangesAsync(default), Times.Never);
}
// Helper method to create mock DbSet
private Mock<DbSet<T>> CreateMockDbSet<T>(IEnumerable<T> data) where T : class
{
var queryable = data.AsQueryable();
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator());
return mockSet;
}
}
}
Integration Test Example: API Endpoint Testing¶
PlansIntegrationTests.cs:
using Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net.Http.Json;
using System.Net;
namespace HappyCamperPlanner.Tests.Integration
{
public class PlansIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public PlansIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace production DbContext with test database
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("TestDb");
});
});
});
_client = _factory.CreateClient();
}
[Fact]
public async Task GetPlans_WithAuthentication_ReturnsOk()
{
// Arrange
var token = await GetTestAuthToken();
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.GetAsync("/api/plans");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var plans = await response.Content.ReadFromJsonAsync<List<PlanDto>>();
Assert.NotNull(plans);
}
[Fact]
public async Task GetPlans_WithoutAuthentication_ReturnsUnauthorized()
{
// Act
var response = await _client.GetAsync("/api/plans");
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task CreatePlan_WithValidData_ReturnsCreated()
{
// Arrange
var token = await GetTestAuthToken();
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var newPlan = new PlanDto
{
Name = "Integration Test Plan",
Description = "Created by integration test",
SeasonYear = 2026
};
// Act
var response = await _client.PostAsJsonAsync("/api/plans", newPlan);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var location = response.Headers.Location;
Assert.NotNull(location);
var createdPlan = await response.Content.ReadFromJsonAsync<PlanDto>();
Assert.Equal("Integration Test Plan", createdPlan.Name);
Assert.True(createdPlan.Id > 0);
}
[Fact]
public async Task GetPlan_AfterCreate_ReturnsCorrectData()
{
// Arrange
var token = await GetTestAuthToken();
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Create a plan
var newPlan = new PlanDto { Name = "Test Plan", SeasonYear = 2026 };
var createResponse = await _client.PostAsJsonAsync("/api/plans", newPlan);
var createdPlan = await createResponse.Content.ReadFromJsonAsync<PlanDto>();
// Act - Retrieve the plan
var getResponse = await _client.GetAsync($"/api/plans/{createdPlan.Id}");
// Assert
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var retrievedPlan = await getResponse.Content.ReadFromJsonAsync<PlanDto>();
Assert.Equal(createdPlan.Id, retrievedPlan.Id);
Assert.Equal("Test Plan", retrievedPlan.Name);
}
[Fact]
public async Task UpdatePlan_WithValidData_ReturnsNoContent()
{
// Arrange
var token = await GetTestAuthToken();
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Create a plan first
var newPlan = new PlanDto { Name = "Original Name", SeasonYear = 2026 };
var createResponse = await _client.PostAsJsonAsync("/api/plans", newPlan);
var createdPlan = await createResponse.Content.ReadFromJsonAsync<PlanDto>();
// Update the plan
createdPlan.Name = "Updated Name";
// Act
var updateResponse = await _client.PutAsJsonAsync(
$"/api/plans/{createdPlan.Id}", createdPlan);
// Assert
Assert.Equal(HttpStatusCode.NoContent, updateResponse.StatusCode);
// Verify the update
var getResponse = await _client.GetAsync($"/api/plans/{createdPlan.Id}");
var updatedPlan = await getResponse.Content.ReadFromJsonAsync<PlanDto>();
Assert.Equal("Updated Name", updatedPlan.Name);
}
[Fact]
public async Task DeletePlan_AsCreator_ReturnsNoContent()
{
// Arrange
var token = await GetTestAuthToken();
_client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Create a plan
var newPlan = new PlanDto { Name = "Plan to Delete", SeasonYear = 2026 };
var createResponse = await _client.PostAsJsonAsync("/api/plans", newPlan);
var createdPlan = await createResponse.Content.ReadFromJsonAsync<PlanDto>();
// Act
var deleteResponse = await _client.DeleteAsync($"/api/plans/{createdPlan.Id}");
// Assert
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
// Verify deletion
var getResponse = await _client.GetAsync($"/api/plans/{createdPlan.Id}");
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
private async Task<string> GetTestAuthToken()
{
// In real scenario, authenticate with Firebase
// For testing, use a test token or mock authentication
return "test-jwt-token";
}
}
}
Test Data Builder Pattern¶
PlanTestBuilder.cs:
namespace HappyCamperPlanner.Tests.Builders
{
public class PlanTestBuilder
{
private int _id = 1;
private string _name = "Test Plan";
private string _description = "Test Description";
private string _creatorId = "test-user-123";
private int _seasonYear = 2026;
private List<Trip> _trips = new();
private List<PlanMember> _members = new();
public PlanTestBuilder WithId(int id)
{
_id = id;
return this;
}
public PlanTestBuilder WithName(string name)
{
_name = name;
return this;
}
public PlanTestBuilder WithCreator(string creatorId)
{
_creatorId = creatorId;
return this;
}
public PlanTestBuilder WithTrips(params Trip[] trips)
{
_trips.AddRange(trips);
return this;
}
public PlanTestBuilder WithMember(string userId, PermissionLevel permission)
{
_members.Add(new PlanMember
{
PlanId = _id,
UserId = userId,
PermissionLevel = permission
});
return this;
}
public Plan Build()
{
return new Plan
{
Id = _id,
Name = _name,
Description = _description,
CreatorId = _creatorId,
SeasonYear = _seasonYear,
Trips = _trips,
Members = _members,
CreatedAt = DateTime.UtcNow
};
}
}
// Usage in tests:
public class PlanServiceTests
{
[Fact]
public async Task CanRetrievePlanWithMultipleTrips()
{
// Arrange
var plan = new PlanTestBuilder()
.WithId(1)
.WithName("Summer Adventure")
.WithCreator("user-123")
.WithTrips(
new Trip { Id = 1, Destination = "Yosemite" },
new Trip { Id = 2, Destination = "Yellowstone" }
)
.WithMember("user-456", PermissionLevel.Editor)
.Build();
// Act & Assert...
}
}
}
Frontend Testing Patterns¶
Component Unit Test Example¶
PlanCard.test.jsx:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PlanCard } from './PlanCard';
describe('PlanCard', () => {
const mockPlan = {
id: 1,
name: 'Summer Camping',
description: 'Family camping trip',
seasonYear: 2026,
tripCount: 3,
memberCount: 5
};
const mockOnEdit = jest.fn();
const mockOnDelete = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
test('renders plan information correctly', () => {
render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
expect(screen.getByText('Summer Camping')).toBeInTheDocument();
expect(screen.getByText('Family camping trip')).toBeInTheDocument();
expect(screen.getByText('3 trips')).toBeInTheDocument();
expect(screen.getByText('5 members')).toBeInTheDocument();
});
test('calls onEdit when edit button is clicked', async () => {
const user = userEvent.setup();
render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
const editButton = screen.getByRole('button', { name: /edit/i });
await user.click(editButton);
expect(mockOnEdit).toHaveBeenCalledTimes(1);
expect(mockOnEdit).toHaveBeenCalledWith(mockPlan.id);
});
test('calls onDelete when delete button is clicked', async () => {
const user = userEvent.setup();
render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);
expect(mockOnDelete).toHaveBeenCalledTimes(1);
expect(mockOnDelete).toHaveBeenCalledWith(mockPlan.id);
});
test('shows confirmation dialog before delete', async () => {
const user = userEvent.setup();
window.confirm = jest.fn(() => true);
render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);
expect(window.confirm).toHaveBeenCalledWith(
'Are you sure you want to delete "Summer Camping"?'
);
expect(mockOnDelete).toHaveBeenCalled();
});
test('cancels delete when confirmation is denied', async () => {
const user = userEvent.setup();
window.confirm = jest.fn(() => false);
render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);
expect(window.confirm).toHaveBeenCalled();
expect(mockOnDelete).not.toHaveBeenCalled();
});
test('applies correct CSS classes based on props', () => {
const { rerender } = render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
isSelected={false}
/>
);
let card = screen.getByTestId('plan-card');
expect(card).not.toHaveClass('selected');
rerender(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
isSelected={true}
/>
);
card = screen.getByTestId('plan-card');
expect(card).toHaveClass('selected');
});
test('is accessible with keyboard navigation', async () => {
const user = userEvent.setup();
render(
<PlanCard
plan={mockPlan}
onEdit={mockOnEdit}
onDelete={mockOnDelete}
/>
);
const editButton = screen.getByRole('button', { name: /edit/i });
// Tab to button
await user.tab();
expect(editButton).toHaveFocus();
// Press Enter
await user.keyboard('{Enter}');
expect(mockOnEdit).toHaveBeenCalled();
});
});
API Integration Test with MSW¶
PlanList.test.jsx:
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { PlanList } from './PlanList';
const mockPlans = [
{ id: 1, name: 'Summer Trip', tripCount: 3, memberCount: 5 },
{ id: 2, name: 'Fall Adventure', tripCount: 2, memberCount: 3 }
];
// Setup MSW server
const server = setupServer(
rest.get('/api/plans', (req, res, ctx) => {
return res(ctx.json(mockPlans));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('PlanList', () => {
test('displays loading state initially', () => {
render(<PlanList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
test('displays plans after successful API call', async () => {
render(<PlanList />);
await waitFor(() => {
expect(screen.getByText('Summer Trip')).toBeInTheDocument();
expect(screen.getByText('Fall Adventure')).toBeInTheDocument();
});
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
test('displays error message when API call fails', async () => {
server.use(
rest.get('/api/plans', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);
render(<PlanList />);
await waitFor(() => {
expect(screen.getByText(/error loading plans/i)).toBeInTheDocument();
});
});
test('displays empty state when no plans exist', async () => {
server.use(
rest.get('/api/plans', (req, res, ctx) => {
return res(ctx.json([]));
})
);
render(<PlanList />);
await waitFor(() => {
expect(screen.getByText(/no plans yet/i)).toBeInTheDocument();
});
});
test('retries API call when retry button is clicked', async () => {
const user = userEvent.setup();
let callCount = 0;
server.use(
rest.get('/api/plans', (req, res, ctx) => {
callCount++;
if (callCount === 1) {
return res(ctx.status(500));
}
return res(ctx.json(mockPlans));
})
);
render(<PlanList />);
await waitFor(() => {
expect(screen.getByText(/error loading plans/i)).toBeInTheDocument();
});
const retryButton = screen.getByRole('button', { name: /retry/i });
await user.click(retryButton);
await waitFor(() => {
expect(screen.getByText('Summer Trip')).toBeInTheDocument();
});
});
});
Custom Hook Testing¶
usePlans.test.js:
import { renderHook, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { usePlans } from './usePlans';
const server = setupServer(
rest.get('/api/plans', (req, res, ctx) => {
return res(ctx.json([
{ id: 1, name: 'Plan 1' },
{ id: 2, name: 'Plan 2' }
]));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('usePlans', () => {
test('returns loading state initially', () => {
const { result } = renderHook(() => usePlans());
expect(result.current.loading).toBe(true);
expect(result.current.plans).toEqual([]);
expect(result.current.error).toBeNull();
});
test('loads plans successfully', async () => {
const { result } = renderHook(() => usePlans());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.plans).toHaveLength(2);
expect(result.current.plans[0].name).toBe('Plan 1');
expect(result.current.error).toBeNull();
});
test('handles API error', async () => {
server.use(
rest.get('/api/plans', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);
const { result } = renderHook(() => usePlans());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.plans).toEqual([]);
expect(result.current.error).toBeTruthy();
});
test('refetches data when refetch is called', async () => {
let callCount = 0;
server.use(
rest.get('/api/plans', (req, res, ctx) => {
callCount++;
return res(ctx.json([{ id: callCount, name: `Plan ${callCount}` }]));
})
);
const { result } = renderHook(() => usePlans());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.plans[0].id).toBe(1);
// Call refetch
act(() => {
result.current.refetch();
});
await waitFor(() => {
expect(result.current.plans[0].id).toBe(2);
});
expect(callCount).toBe(2);
});
});
Testing Best Practices¶
Test Naming Convention¶
// ❌ Bad test names
[Fact]
public void Test1() { }
[Fact]
public void TestGetPlan() { }
// ✅ Good test names - describe what is being tested and expected outcome
[Fact]
public void GetPlan_WithValidId_ReturnsOkResult() { }
[Fact]
public void GetPlan_WithNonExistentId_ReturnsNotFound() { }
[Fact]
public void GetPlan_WithUnauthorizedUser_ReturnsForbidden() { }
[Fact]
public void CreatePlan_WithEmptyName_ReturnsBadRequest() { }
AAA Pattern (Arrange, Act, Assert)¶
[Fact]
public async Task Example_Test()
{
// Arrange - Set up test data and dependencies
var planId = 1;
var mockPlan = new Plan { Id = planId, Name = "Test" };
_mockContext.Setup(c => c.Plans.FindAsync(planId)).ReturnsAsync(mockPlan);
// Act - Execute the method being tested
var result = await _controller.GetPlan(planId);
// Assert - Verify the expected outcome
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var planDto = Assert.IsType<PlanDto>(okResult.Value);
Assert.Equal(planId, planDto.Id);
}
Test Independence¶
// ✅ Each test is independent and can run in any order
public class IndependentTests
{
private readonly PlansController _controller;
public IndependentTests()
{
// Fresh setup for each test
_controller = CreateController();
}
[Fact]
public void Test1()
{
// This test doesn't rely on Test2
}
[Fact]
public void Test2()
{
// This test doesn't rely on Test1
}
}
Test Fixtures for Shared Setup¶
public class DatabaseFixture : IDisposable
{
public ApplicationDbContext Context { get; private set; }
public DatabaseFixture()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
Context = new ApplicationDbContext(options);
// Seed test data
SeedTestData();
}
private void SeedTestData()
{
Context.Plans.AddRange(
new Plan { Id = 1, Name = "Plan 1", CreatorId = "user-1" },
new Plan { Id = 2, Name = "Plan 2", CreatorId = "user-2" }
);
Context.SaveChanges();
}
public void Dispose()
{
Context.Dispose();
}
}
// Use fixture in test class
public class PlanTests : IClassFixture<DatabaseFixture>
{
private readonly ApplicationDbContext _context;
public PlanTests(DatabaseFixture fixture)
{
_context = fixture.Context;
}
[Fact]
public async Task CanRetrieveSeededPlans()
{
var plans = await _context.Plans.ToListAsync();
Assert.Equal(2, plans.Count);
}
}
Code Coverage¶
Measuring Coverage¶
In .NET:
# Install coverage tool
dotnet tool install --global coverlet.console
# Run tests with coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
# Generate HTML report
reportgenerator -reports:coverage.opencover.xml -targetdir:coveragereport
In React:
# Jest includes coverage built-in
npm test -- --coverage
# View coverage report
open coverage/lcov-report/index.html
Coverage Goals¶
- Critical paths: 100% coverage
- Business logic: 90%+ coverage
- Controllers/Components: 80%+ coverage
- Overall project: 70%+ coverage
Coverage Configuration¶
coverlet.runsettings:
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat Code Coverage">
<Configuration>
<Format>opencover</Format>
<Exclude>[*]*.Program,[*]*.Startup</Exclude>
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute</ExcludeByAttribute>
<ExcludeByFile>**/Migrations/*.cs</ExcludeByFile>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
jest.config.js:
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/**/*.test.{js,jsx}',
'!src/**/__tests__/**'
],
coverageThresholds: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
};
Testing Checklist¶
Unit Testing ✅¶
- All public methods have tests
- Edge cases covered (null, empty, boundary values)
- Error scenarios tested
- Authorization checks tested
- Input validation tested
- Tests follow AAA pattern
- Tests are independent
- Mocks used appropriately
Integration Testing ✅¶
- API endpoints tested end-to-end
- Authentication flow tested
- Authorization rules verified
- Database interactions tested
- Error responses validated
- Success paths verified
- HTTP status codes correct
Frontend Testing ✅¶
- Components render correctly
- User interactions work
- API calls mocked appropriately
- Loading states tested
- Error states tested
- Empty states tested
- Accessibility verified
- Keyboard navigation works
Test Quality ✅¶
- Tests are readable and maintainable
- Test names are descriptive
- No commented-out tests
- No flaky tests
- Fast execution (<5 seconds for unit tests)
- Code coverage meets goals
- Tests run in CI/CD pipeline
Common Testing Scenarios for Happy Camper Planner¶
Testing Authorization Logic¶
[Theory]
[InlineData(PermissionLevel.Viewer, false)]
[InlineData(PermissionLevel.Editor, false)]
[InlineData(PermissionLevel.Admin, true)]
public async Task DeletePlan_WithDifferentPermissionLevels_ReturnsExpectedResult(
PermissionLevel permission, bool shouldSucceed)
{
// Arrange
var planId = 1;
var testPlan = new Plan
{
Id = planId,
CreatorId = "different-user",
Members = new List<PlanMember>
{
new PlanMember
{
UserId = _testUserId,
PermissionLevel = permission
}
}
};
var mockSet = CreateMockDbSet(new[] { testPlan });
_mockContext.Setup(c => c.Plans).Returns(mockSet.Object);
// Act
var result = await _controller.DeletePlan(planId);
// Assert
if (shouldSucceed)
{
Assert.IsType<NoContentResult>(result);
}
else
{
Assert.IsType<ForbidResult>(result);
}
}
Testing RV Compatibility Matching¶
[Theory]
[InlineData(25, 8, 11, "30A", true)] // Fits
[InlineData(35, 8, 11, "30A", false)] // Too long
[InlineData(25, 10, 11, "30A", false)] // Too wide
[InlineData(25, 8, 13, "30A", false)] // Too tall
[InlineData(25, 8, 11, "50A", true)] // Electric OK
public async Task SearchCampgrounds_WithRVSpecs_ReturnsCompatibleSites(
int length, int width, int height, string electrical, bool shouldMatch)
{
// Test RV compatibility algorithm
}
When to Use Testing Specialist¶
Use this agent when:
- Creating unit tests for new features
- Writing integration tests for API endpoints
- Testing React components and hooks
- Improving code coverage in specific areas
- Debugging failing tests
- Refactoring tests for maintainability
- Setting up test infrastructure (fixtures, mocks)
- Test-driven development (write tests first)
- Adding regression tests for bugs
- Performance testing critical paths
Integration with Baseline Behaviors¶
This agent follows all baseline behaviors from ../context/LLM-BaselineBehaviors.md:
- Action-oriented: Creates actual test code, not just descriptions
- Research-driven: Examines code to understand what needs testing
- Complete solutions: Provides full test implementations
- Clear communication: Explains test purpose and approach
- Error handling: Tests error scenarios thoroughly
- Task management: Systematically covers test cases
Testing-specific additions: - AAA pattern: Arrange, Act, Assert structure - Independence: Each test stands alone - Coverage-focused: Aims for comprehensive test coverage - Maintainability: Writes readable, sustainable tests - Fast execution: Optimizes for quick test runs - Practical: Balances coverage with pragmatism