Acceptance Testing Strategies (Part 2): Feature Tests, Pyramid, and Best Practices

Published: (March 20, 2026 at 05:20 AM EDT)
6 min read
Source: Dev.to

Source: Dev.to

In Part 1 we covered what acceptance tests are, plus inside-out and outside-in testing. Here we add the third strategy (feature tests) then compare all three, look at the testing pyramid, and share best practices for combining them. ← Part 1: Inside-Out and Outside-In Feature tests are comprehensive tests that validate complete features or user stories from end to end. They combine elements of both inside-out and outside-in testing to ensure features work correctly across all layers of the application. Think of feature tests as the “final exam” for a complete feature—they verify everything works together as expected. Feature-focused: Tests complete features, not individual components Cross-layer: Spans multiple layers of the application Business-driven: Based on user stories and business requirements Integration-heavy: Tests how different components work together public class OrderManagementFeatureTests : IClassFixture> { private readonly WebApplicationFactory _factory; private readonly HttpClient _client;

public OrderManagementFeatureTests(WebApplicationFactory factory)
{
    _factory = factory;
    _client = _factory.CreateClient();
}

[Fact]
public async Task Feature_OrderManagement_CompleteOrderLifecycle()
{
    // Arrange: Create test data
    var customer = await CreateTestCustomer();
    var product = await CreateTestProduct();

    // 1. Create Order
    var createOrderRequest = new CreateOrderRequest
    {
        CustomerId = customer.Id,
        Items = new List
        {
            new OrderItemRequest { ProductId = product.Id, Quantity = 2 }
        }
    };

    var createResponse = await _client.PostAsJsonAsync("/api/orders", createOrderRequest);
    createResponse.EnsureSuccessStatusCode();
    var order = await createResponse.Content.ReadFromJsonAsync();

    Assert.NotNull(order);
    Assert.Equal(OrderStatus.Pending, order.Status);

    // 2. Process Payment
    var paymentRequest = new ProcessPaymentRequest
    {
        OrderId = order.Id,
        PaymentMethod = "credit_card",
        Amount = order.Total
    };

    var paymentResponse = await _client.PostAsJsonAsync("/api/payments", paymentRequest);
    paymentResponse.EnsureSuccessStatusCode();
    var payment = await paymentResponse.Content.ReadFromJsonAsync();

    Assert.True(payment.IsSuccessful);

    // 3. Confirm Order
    var confirmResponse = await _client.PostAsync($"/api/orders/{order.Id}/confirm", null);
    confirmResponse.EnsureSuccessStatusCode();

    // 4. Verify Order Status
    var orderResponse = await _client.GetAsync($"/api/orders/{order.Id}");
    orderResponse.EnsureSuccessStatusCode();
    var confirmedOrder = await orderResponse.Content.ReadFromJsonAsync();

    Assert.Equal(OrderStatus.Confirmed, confirmedOrder.Status);
}

private async Task CreateTestCustomer()
{
    var customerRequest = new CreateCustomerRequest
    {
        Name = "Test Customer",
        Email = "test@example.com"
    };

    var response = await _client.PostAsJsonAsync("/api/customers", customerRequest);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync();
}

private async Task CreateTestProduct()
{
    var productRequest = new CreateProductRequest
    {
        Name = "Test Product",
        Price = 29.99m,
        Stock = 100
    };

    var response = await _client.PostAsJsonAsync("/api/products", productRequest);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync();
}

}

✅ Complete Coverage: Tests entire features end-to-end ✅ Business Value: Ensures features deliver expected business value ✅ Integration Validation: Tests how different components work together ✅ User Story Validation: Directly maps to user stories and requirements ✅ Regression Prevention: Prevents feature regressions ❌ Slow Execution: Feature tests are typically slower than unit tests ❌ Complex Setup: Requires more complex test data and environment setup ❌ Maintenance Overhead: Can be harder to maintain as features evolve ❌ Debugging Difficulty: Failures can be harder to debug and isolate Critical business features User story validation Integration testing Regression testing Release validation

Aspect Inside-Out Outside-In Feature Tests

Starting Point Core business logic User interface Complete features

Focus Technical quality User experience Business value

Test Speed Fast (unit tests) Slow (UI tests) Medium (integration)

Maintenance Easy Hard Medium

Business Alignment Low High High

Technical Coverage High Medium Medium

Feedback Speed Fast Slow Medium

The pyramid reminds us to have few slow, expensive tests at the top and many fast, cheap tests at the bottom: E2E / Feature tests (few, slow, high value) /
/
/----
/
Integration / API \ (some, medium) / tests
/------------
/
Unit tests / (many, fast) \ ← base of the pyramid /__________________\

Layer Count Speed Purpose

Top Few Slow E2E / feature tests; full user journey, high confidence

Middle Some Medium Integration / API tests; component interaction

Bottom Many Fast Unit tests; technical quality, component isolation

In practice, unit tests run on every commit or PR; integration/API tests often run in the same pipeline or on merge; acceptance/feature/E2E tests may run on a schedule (e.g. nightly), on release branches, or i

n a staging environment because they’re slower and need more infrastructure. Balancing speed and confidence here is a key responsibility for SDETs and DevOps. Combine all three strategies for comprehensive coverage: // 1. Unit Tests (Inside-Out) - Fast feedback, technical quality [Fact] public void OrderService_CalculateTotal_WithValidItems_ReturnsCorrectTotal() { // Test core business logic }

// 2. Integration Tests (Inside-Out) - Component interaction [Fact] public async Task OrderController_CreateOrder_WithValidRequest_ReturnsCreatedOrder() { // Test API layer }

// 3. Feature Tests (Outside-In) - Business value validation [Fact] public async Task Feature_OrderManagement_CompleteOrderLifecycle() { // Test complete feature }

// xUnit: use nested classes or separate test classes by feature public class OrderCreationTests { [Fact] public void CreateOrder_WithValidData_ReturnsSuccess() { }

[Fact]
public void CreateOrder_WithInvalidData_ReturnsValidationError() { }

}

public class OrderProcessingTests { [Fact] public void ProcessOrder_WithValidPayment_ConfirmsOrder() { } }

public class OrderBuilder { private Order _order = new Order();

public OrderBuilder WithCustomer(int customerId)
{
    _order.CustomerId = customerId;
    return this;
}

public OrderBuilder WithItem(int productId, int quantity)
{
    _order.Items.Add(new OrderItem { ProductId = productId, Quantity = quantity });
    return this;
}

public Order Build() => _order;

}

// Usage var order = new OrderBuilder() .WithCustomer(123) .WithItem(1, 2) .WithItem(2, 1) .Build();

Each test should be able to run independently without relying on other tests. Use setup and teardown methods to ensure clean test state. For UI acceptance tests, prefer stable selectors (e.g. role, label, test IDs used sparingly) and avoid coupling to implementation details so tests survive refactors; SDETs often use Page Object Model or similar to keep automation maintainable. Test names should clearly describe what is being tested (and, for acceptance tests, ideally reflect the scenario or acceptance criterion): // Good [Fact] public void Customer_Can_Complete_Purchase_WithValidPaymentDetails()

// Bad [Fact] public void Test1()

For most projects, a hybrid approach works best: Start with Outside-In for new features to ensure business alignment Use Inside-Out for technical debt reduction and component quality Implement Feature Tests for critical business functionality Maintain a healthy test pyramid with more unit tests than integration tests Acceptance testing is crucial for ensuring your software meets business requirements and delivers value to users. The choice between inside-out, outside-in, and feature testing approaches depends on your specific needs, team structure, and project requirements. Key Takeaways: Inside-Out Testing is ideal for technical quality and component isolation Outside-In Testing focuses on user experience and business value Feature Tests provide comprehensive end-to-end validation Hybrid Approach combining all three strategies offers the best coverage Remember: The goal is not to use every testing approach, but to find the right combination that ensures quality, meets business needs, and fits your team’s capabilities. Start by assessing your current testing strategy, identify gaps, and gradually introduce the approaches that make sense for your project. Happy testing! 🚀 ← Part 1: Inside-Out and Outside-In

0 views
Back to Blog

Related posts

Read more »