Portal Community

Testing Strategy

Test LayerWhat It TestsTools
Unit testsHandler logic, error handling, JSON serializationxUnit, Moq, FluentAssertions
Contract testsGET /tools schema validity, POST /tool/.../call wire formatWebApplicationFactory, JsonSchema.Net
Integration testsReal external API calls (against sandbox)xUnit + test Zendesk tenant
Curl smoke testsQuick manual verification of running servercurl / HTTPie
Agent end-to-endAgent calls the tool via Octopus; correct LLM behaviourOctopus test agent + staging MCP server

Unit Testing Tool Handlers

public class ZendeskToolHandlerTests
{
    private readonly Mock<IZendeskClient> _zendesk = new();
    private readonly Mock<ILogger<ZendeskToolHandler>> _logger = new();
    private ZendeskToolHandler CreateHandler()
        => new(_zendesk.Object, _logger.Object);

    [Fact]
    public async Task CreateTicket_Returns_TicketId_On_Success()
    {
        // Arrange
        _zendesk.Setup(z => z.CreateTicketAsync(
                It.IsAny<string>(),
                It.IsAny<CreateTicketRequest>(),
                It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ZendeskTicket
            {
                Id  = 12345,
                Url = "https://mycompany.zendesk.com/tickets/12345",
                Status = "open"
            });

        var args = JsonDocument.Parse("""
        {
          "subject":     "Cannot log in",
          "description": "Getting 401 on /auth/login",
          "priority":    "high"
        }
        """).RootElement;

        var ctx = new DefaultHttpContext();
        ctx.Request.Headers["X-Octopus-Tenant-Id"] = "tenant-abc";

        var handler = CreateHandler();

        // Act
        var result = await handler.HandleAsync(
            "zendesk_create_ticket", args, ctx, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result!.Value.GetProperty("ticket_id").GetInt32().Should().Be(12345);
        result.Value.GetProperty("status").GetString().Should().Be("open");
    }

    [Fact]
    public async Task CreateTicket_Returns_Error_On_RateLimit()
    {
        _zendesk.Setup(z => z.CreateTicketAsync(
                It.IsAny<string>(),
                It.IsAny<CreateTicketRequest>(),
                It.IsAny<CancellationToken>()))
            .ThrowsAsync(new ZendeskRateLimitException());

        var args = JsonDocument.Parse("""
        { "subject": "Test", "description": "Test" }
        """).RootElement;

        var handler = CreateHandler();
        var result = await handler.HandleAsync(
            "zendesk_create_ticket", args, new DefaultHttpContext(), CancellationToken.None);

        result!.Value.GetProperty("error").GetString().Should().Be("rate_limit");
        result.Value.GetProperty("retry_after").GetInt32().Should().BePositive();
    }
}

Contract Testing the Wire Format

public class McpContractTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public McpContractTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
        _client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", "test-token");
    }

    [Fact]
    public async Task GET_tools_Returns_Valid_ToolDescriptors()
    {
        var response = await _client.GetAsync("/tools");
        response.EnsureSuccessStatusCode();

        var body    = await response.Content.ReadAsStringAsync();
        var tools   = JsonSerializer.Deserialize<JsonElement[]>(body)!;

        tools.Should().NotBeEmpty();

        foreach (var tool in tools)
        {
            // Every tool must have name, description, inputSchema
            tool.GetProperty("name").GetString().Should().NotBeNullOrEmpty();
            tool.GetProperty("description").GetString().Should().NotBeNullOrEmpty();
            tool.TryGetProperty("inputSchema", out _).Should().BeTrue();

            // inputSchema must be a valid object type
            var schema = tool.GetProperty("inputSchema");
            schema.GetProperty("type").GetString().Should().Be("object");
            schema.TryGetProperty("properties", out _).Should().BeTrue();
        }
    }

    [Fact]
    public async Task POST_tool_call_Returns_200_With_Result_Object()
    {
        var payload = JsonContent.Create(new
        {
            subject     = "Integration test ticket",
            description = "Created by contract test",
            priority    = "low"
        });

        var response = await _client.PostAsync(
            "/tool/zendesk_create_ticket/call", payload);

        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var body   = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<JsonElement>(body);

        // Either success with ticket_id, or a structured error — never an exception
        var hasTicketId = result.TryGetProperty("ticket_id", out _);
        var hasError    = result.TryGetProperty("error", out _);

        (hasTicketId || hasError).Should().BeTrue("result must have ticket_id or error");
    }

    [Fact]
    public async Task POST_unknown_tool_Returns_404()
    {
        var response = await _client.PostAsync(
            "/tool/does_not_exist/call",
            JsonContent.Create(new { }));

        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}

Curl Smoke Tests

# 1. Health check
curl -s https://mcp-zendesk.internal/health
# Expected: {"status":"ok","version":"1.0.0"}

# 2. Discover tools
curl -s -H "Authorization: Bearer $TOKEN" \
     https://mcp-zendesk.internal/tools | jq '.[].name'
# Expected: ["zendesk_create_ticket","zendesk_search_tickets","zendesk_get_ticket"]

# 3. Call a tool
curl -s -X POST \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -H "X-Octopus-Tenant-Id: tenant-abc" \
     -d '{"subject":"Test","description":"Curl test","priority":"low"}' \
     https://mcp-zendesk.internal/tool/zendesk_create_ticket/call
# Expected: {"ticket_id":12346,"ticket_url":"...","status":"open"}

# 4. Verify auth enforcement
curl -s https://mcp-zendesk.internal/tools
# Expected: HTTP 401 {"error":"unauthorized","message":"..."}

Common Test Failure Patterns

FailureRoot CauseFix
GET /tools returns empty arrayNo IMcpToolHandler registered in DIRegister handler in AddSingleton<IMcpToolHandler, ...>()
Tool call returns 404CanHandle returns false for the tool nameCheck the tool name matches exactly (case-sensitive)
JSON deserialization fails in handlerMissing property accessed with GetProperty (not TryGetProperty)Use TryGetProperty for optional fields
Auth test passes without tokenAuth middleware not applied to the test routeAdd .RequireAuthorization() to the endpoint
Integration test hits production APITest environment config points to productionUse a test credential and Zendesk sandbox tenant