Skip to content

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