Skip to content

Acceptance Testing — Complete Guide with Examples

DodaTech Updated 2026-06-20 10 min read

In this tutorial, you'll learn about Acceptance Testing. We cover key concepts, practical examples, and best practices.

Acceptance testing is the final validation phase where real-world scenarios confirm that the software meets business requirements, user expectations, and acceptance criteria — ensuring the team builds the right product, not just a correct one.

What You'll Learn

  • The difference between acceptance testing and other test levels
  • How to write acceptance criteria using Given-When-Then format
  • Tools and frameworks for automating acceptance tests
  • How acceptance testing fits into CI/CD pipelines

Why It Matters

A feature that passes unit and integration tests can still fail in the hands of a user. Acceptance testing catches mismatches between what developers built and what stakeholders actually need. Without it, teams ship features that work technically but fail business — costing rework, missed deadlines, and lost revenue.

Real-World Use

A banking application's "transfer funds" feature passed all unit tests. But in acceptance testing, the product owner noticed the confirmation screen showed a transaction ID without the recipient's name. Users couldn't verify they sent money to the right person. The fix — adding recipient details — took two hours. Catching that after launch would have generated thousands of support tickets.

What Is Acceptance Testing?

Acceptance testing evaluates whether a system meets its acceptance criteria — the conditions that define a feature as complete from the stakeholder's perspective. Unlike unit tests that verify a single function, or integration tests that check component interaction, acceptance tests validate end-to-end business scenarios.

There are several types:

Type Who Performs It Focus
User Acceptance Testing (UAT) End users or their representatives Usability and workflow correctness
Business Acceptance Testing Product owners, business analysts Alignment with business requirements
Operational Acceptance Testing Operations team Deployability, backup, monitoring
Contract Acceptance Testing Customers or regulators Contractual and legal obligations
Alpha/Beta Testing Internal teams / external users Pre-release validation

Writing Acceptance Criteria

Acceptance criteria define when a feature is done. They are written in Given-When-Then format, which originated in behavior-driven development (BDD):

Given [some initial context]
When [an action occurs]
Then [expected outcome]

Let's look at an example for a login feature:

Scenario: Successful login with valid credentials
  Given the user is on the login page
  When the user enters a valid username and password
    and clicks the "Sign In" button
  Then the user should be redirected to the dashboard
    and see a welcome message with their name

This format is understandable by non-technical stakeholders and directly translatable into automated tests.

# acceptance_test_login.py — automated acceptance test using pytest
# Requires: pip install pytest

def test_successful_login():
    # Given — set up initial state
    user = {"username": "alice", "password": "secure123"}
    db = {"alice": {"password": "secure123", "name": "Alice"}}

    # When — perform the action
    stored = db.get(user["username"])
    login_ok = stored is not None and stored["password"] == user["password"]

    # Then — verify expected outcome
    assert login_ok is True
    if login_ok:
        print(f"Welcome, {stored['name']}!")

test_successful_login()

Expected output:

Welcome, Alice!

Automated Acceptance Testing with pytest-bdd

For larger projects, automate acceptance tests using BDD frameworks. Here is an example using pytest-bdd:

# test_checkout.py — BDD-style acceptance test for an e-commerce checkout
# Requires: pip install pytest-bdd
from pytest_bdd import scenario, given, when, then, parsers

@scenario("checkout.feature", "Customer completes checkout with valid cart")
def test_checkout():
    pass

@given("the customer has items in the cart", target_fixture="cart")
def cart_with_items():
    cart = {"items": [{"id": 1, "price": 25.0}, {"id": 2, "price": 35.0}]}
    return cart

@given("the customer is logged in", target_fixture="customer")
def logged_in_customer():
    return {"id": 42, "name": "Bob"}

@when("the customer proceeds to checkout")
def proceed_to_checkout(cart, customer):
    cart["customer_id"] = customer["id"]
    cart["status"] = "checkout"

@when(parsers.parse("the customer enters a valid {payment_method}"))
def enter_payment(cart, payment_method):
    cart["payment"] = payment_method

@then(parsers.parse("the order should be confirmed with {expected_total}"))
def verify_order(cart, expected_total):
    total = sum(item["price"] for item in cart["items"])
    assert total == float(expected_total)
    assert cart["status"] == "checkout"
    print(f"Order confirmed. Total: ${total:.2f}")

Expected output when run with pytest test_checkout.py -v -s:

collected 1 item
test_checkout.py::test_checkout Order confirmed. Total: $60.00
PASSED

Acceptance Criteria for Non-Functional Requirements

Acceptance testing isn't only about features. Non-functional requirements (performance, security, usability) need acceptance criteria too:

# acceptance_performance.py — acceptance test for page load time
import time

def measure_response_time(url):
    """Simulates measuring HTTP response time."""
    # In production, use requests.get(url).elapsed
    time.sleep(0.15)  # simulate network latency
    return 0.15

def test_page_loads_under_200ms():
    url = "https://api.example.com/products"
    elapsed = measure_response_time(url)
    assert elapsed < 0.200, f"Page load {elapsed*1000:.0f}ms exceeds 200ms limit"
    print(f"Performance check passed: {elapsed*1000:.0f}ms")

def test_search_returns_results_under_500ms():
    # Simulate a search query
    elapsed = 0.32
    assert elapsed < 0.500, "Search response time exceeds limit"
    print(f"Search performance OK: {elapsed*1000:.0f}ms")

test_page_loads_under_200ms()
test_search_returns_results_under_500ms()

Expected output:

Performance check passed: 150ms
Search performance OK: 320ms

Integrating Acceptance Tests into CI/CD

Automated acceptance tests should run after unit and integration tests pass, typically in a staging environment:

# .github/workflows/acceptance.yml — CI pipeline stage for acceptance tests
name: Acceptance Tests
on:
  deployment_status:
    environments: staging
jobs:
  acceptance:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: |
          pip install pytest pytest-bdd requests
      - name: Run acceptance tests
        run: pytest tests/acceptance/ -v --junitxml=reports/acceptance.xml
      - name: Upload report
        uses: actions/upload-artifact@v4
        with:
          name: acceptance-report
          path: reports/acceptance.xml

Acceptance Testing vs Other Test Levels

Understanding where acceptance testing fits in the test pyramid helps teams allocate effort wisely:

flowchart TB
    subgraph "Test Pyramid"
        E2E["End-to-End / Acceptance Tests (few)"]
        INT["Integration Tests (some)"]
        UNIT["Unit Tests (many)"]
    end

    subgraph "Validation Focus"
        BUS["Business rules, workflows, user satisfaction"]
        API["Component interaction, data flow"]
        LOGIC["Functions, methods, edge cases"]
    end

    UNIT -->|"components combined"| INT
    INT -->|"system deployed"| E2E
    E2E -->|"stakeholder signs off"| PROD[Production Release]

    style E2E fill:#4a90d9,stroke:#fff,color:#fff
    style INT fill:#e67e22,stroke:#fff,color:#fff
    style UNIT fill:#27ae60,stroke:#fff,color:#fff

The pyramid makes an important point: acceptance tests are the slowest and most expensive to maintain, so you write fewer of them. But they provide the highest confidence that the system works for the user.

Creating an Acceptance Test Plan

A structured test plan ensures coverage of all critical workflows:

import json
from datetime import datetime

def generate_acceptance_test_plan(features, output_path):
    plan = {
        "generated_at": datetime.utcnow().isoformat(),
        "total_features": len(features),
        "features": []
    }
    for feature in features:
        entry = {
            "name": feature["name"],
            "priority": feature.get("priority", "medium"),
            "scenarios": []
        }
        for scenario in feature["scenarios"]:
            entry["scenarios"].append({
                "title": scenario["title"],
                "given": scenario["given"],
                "when": scenario["when"],
                "then": scenario["then"],
                "automated": scenario.get("automated", False)
            })
        plan["features"].append(entry)
    with open(output_path, "w") as f:
        json.dump(plan, f, indent=2)
    print(f"Test plan generated: {output_path}")
    print(f"Features: {plan['total_features']}, Scenarios: {sum(len(f['scenarios']) for f in plan['features'])}")

sample_features = [
    {
        "name": "User Authentication",
        "priority": "critical",
        "scenarios": [
            {
                "title": "Successful login",
                "given": "user is on login page",
                "when": "user enters valid credentials",
                "then": "user is redirected to dashboard",
                "automated": True
            }
        ]
    }
]
generate_acceptance_test_plan(sample_features, "/tmp/test-plan.json")

Expected output:

Test plan generated: /tmp/test-plan.json
Features: 1, Scenarios: 1

Security-Focused Acceptance Testing

Security acceptance criteria ensure that features don't introduce vulnerabilities. This aligns with the DodaTech approach of treating security as a quality attribute:

Scenario: Password change prevents reuse of last 5 passwords
  Given the user's password history contains "Pass#2023", "Pass#2024"
  When the user tries to change password to "Pass#2024"
  Then the system should reject the change
    and display "Password has been used recently"

Scenario: API endpoint rejects unauthenticated requests
  Given no authentication token is provided
  When the client sends a GET request to /api/orders
  Then the response status should be 401
    and the body should contain "Unauthorized"
# test_security_acceptance.py — security-related acceptance criteria
import json

def test_password_reuse_prevention():
    history = ["Pass#2023", "Pass#2024"]
    new_password = "Pass#2024"

    is_reused = new_password in history
    assert is_reused is True, "Password was not in history but should have been"
    if is_reused:
        print("Password rejected: has been used recently")

def test_api_unauthorized_access():
    headers = {}  # no auth token
    response_status = 401  # simulated
    response_body = json.dumps({"error": "Unauthorized"})

    assert response_status == 401
    body = json.loads(response_body)
    assert "Unauthorized" in body["error"]
    print(f"API correctly rejected request with status {response_status}")

test_password_reuse_prevention()
test_api_unauthorized_access()

Expected output:

Password rejected: has been used recently
API correctly rejected request with status 401

Common Errors in Acceptance Testing

# Mistake Explanation Fix
1 Writing criteria after coding Acceptance criteria must exist before development starts Write criteria during refinement, not after implementation
2 Criteria too vague "The page should load fast" is not testable Use specific thresholds: "Page loads under 2 seconds on 3G"
3 Automating everything UAT needs human judgment — not all acceptance tests should be automated Automate regression acceptance; keep exploratory UAT manual
4 No stakeholder sign-off Automated tests pass but product owner never validated the workflow Schedule UAT sessions before the release candidate
5 Testing only happy paths Acceptance tests that always pass give false confidence Include negative scenarios: invalid data, timeouts, permission errors
6 Skipping non-functional criteria Performance, security, accessibility are left out of acceptance Add NFR acceptance criteria as separate scenarios

Learning Path

flowchart LR
    A[Integration Testing] --> B[Acceptance Testing]
    B --> C[Test Automation Frameworks]
    A --> D[Continuous Testing]
    D --> B
    B --> E[Production Monitoring]
    C --> E
    style B fill:#4a90d9,stroke:#fff,color:#fff
    style A fill:#e67e22,stroke:#fff,color:#fff
    style D fill:#e67e22,stroke:#fff,color:#fff

Before acceptance testing, you should understand Integration Testing and Continuous Testing. After mastery, explore Test Automation Frameworks for scaling your test suite.

Practice Questions

1. What format is used to write acceptance criteria? Given-When-Then format, originating from behavior-driven development (BDD).
2. How does acceptance testing differ from unit testing? Unit testing verifies that individual functions work correctly. Acceptance testing validates that the system meets business requirements from the user's perspective.
3. What is the role of the product owner in acceptance testing? The product owner defines acceptance criteria, reviews test results, and makes the final decision on whether a feature meets the business need.
4. Why should acceptance tests be fewer than unit tests? Acceptance tests are slower, more expensive to maintain, and test broader scenarios. Writing too many leads to brittle, slow test suites.
5. What is the difference between UAT and operational acceptance testing? UAT focuses on whether end users can accomplish their goals. Operational acceptance testing focuses on whether the operations team can deploy, monitor, and support the system.

Challenge

Write a complete acceptance test suite for a "password reset" feature using pytest-bdd. Include at least 4 scenarios: successful reset, expired token, mismatched passwords, and rate-limiting for too many requests.

Real-World Task

Pick a feature from a project you work on. Write its acceptance criteria in Given-When-Then format. Then automate one scenario as a pytest test. Share both the criteria and the automated test with your team for feedback.

FAQ

What is acceptance testing in agile?

In agile, acceptance testing is a continuous activity. Product owners define acceptance criteria for each user story before development starts. Developers and testers then validate each story against those criteria during the sprint.

Who writes acceptance tests?

Acceptance criteria are written collaboratively — product owners define the business conditions, while QA engineers and developers translate them into automated tests. UAT scenarios are typically executed by business representatives.

Can acceptance testing be fully automated?

Not entirely. While regression acceptance tests should be automated, exploratory UAT requires human judgment to evaluate usability, visual design, and subjective business fit. Automate the repeatable checks; keep the creative discovery manual.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Next lesson: Test Automation Frameworks — explore tools for scaling your automated test suite across unit, integration, and acceptance levels.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro