# Delete File Source: https://docs.loadforge.com/api-reference/endpoint/file-delete DELETE /files/{id} Delete a file Delete a file by ID. # Get File Source: https://docs.loadforge.com/api-reference/endpoint/file-get GET /files/{id} Get a file by ID Fetch a file by ID. Returns 404 if not found. # Update File Source: https://docs.loadforge.com/api-reference/endpoint/file-update PATCH /files/{id} Update a file. Changing extension regenerates stored filename. Update a file. `name` must remain unique per team and keep a valid extension. If the extension changes, a new stored filename is generated. # Create File Source: https://docs.loadforge.com/api-reference/endpoint/files-create POST /files Create a file. Content may be plain text or base64 (auto-detected) Create a team-scoped file. `content` can be plain text or base64 (auto-detected). `name` must be unique per team and end with `.csv`, `.json`, or `.txt`. # List Files Source: https://docs.loadforge.com/api-reference/endpoint/files-list GET /files List files for the current team (ordered by name) Retrieve all team files (CSV, JSON, TXT), ordered by `name`. # Create Host Source: https://docs.loadforge.com/api-reference/endpoint/hosts-create POST /hosts Create a new host by providing necessary details Create a new host by providing necessary details such as `url`, `port`, and `protocol`. Post-creation, the host will be automatically scheduled for verification. # Delete Host Source: https://docs.loadforge.com/api-reference/endpoint/hosts-delete DELETE /hosts/{host_id} Delete a host entry by specifying the host_id Delete a host entry by specifying the `host_id` in the API endpoint. Ensure that all tests utilizing the host are deleted beforehand to prevent an error response. # List Hosts Source: https://docs.loadforge.com/api-reference/endpoint/hosts-list GET /hosts Retrieve a list of all hosts registered on your account Retrieve a list of all hosts registered on your account, including their respective details like `id`, creation timestamp, verification status, URL, port, and protocol. # Create Library Source: https://docs.loadforge.com/api-reference/endpoint/library-create POST /library Create a library. Content may be plain text or base64 (auto-detected) Create a team Python library file. `content` can be plain text or base64 (auto-detected). `name` must be unique per team and end with `.py`. # Delete Library Source: https://docs.loadforge.com/api-reference/endpoint/library-delete DELETE /library/{id} Delete a library Delete a library by ID. # Get Library Source: https://docs.loadforge.com/api-reference/endpoint/library-get GET /library/{id} Get a library by ID Fetch a library by ID. Returns 404 if not found. # List Libraries Source: https://docs.loadforge.com/api-reference/endpoint/library-list GET /library List libraries for the current team (ordered by name) Retrieve all team Python library files, ordered by `name`. # Update Library Source: https://docs.loadforge.com/api-reference/endpoint/library-update PATCH /library/{id} Update a library Update a library. `name` must remain unique per team and end with `.py`. # Get Result Source: https://docs.loadforge.com/api-reference/endpoint/result-get GET /result/{id} Returns the full data for a single Result set Returns the full data for a single Result set. This endpoint provides detailed information about a specific test run result, including CSV statistics, history, and failures data. # List Results Source: https://docs.loadforge.com/api-reference/endpoint/results-list GET /results Returns a summary of your results Returns a summary of your results. This endpoint provides a list of all your result sets, with one set of results for each run. A run is an individual instance of a test being executed. # Cancel Run Source: https://docs.loadforge.com/api-reference/endpoint/run-cancel POST /run/cancel/{run_id} Cancel a run Cancel an ongoing run by specifying the run\_id. This endpoint allows you to stop a test run that is in progress if you need to terminate it before its scheduled completion. # Start Run Source: https://docs.loadforge.com/api-reference/endpoint/run-start POST /run Start a run Start a run of a specified test. The primary requirements are to specify the ID of a test (which can be obtained via the Test API or in the UI) and the duration in minutes, from 2 minutes to 720 minutes. The endpoint returns a run\_id which can be used to monitor responses afterwards. # Create Test Source: https://docs.loadforge.com/api-reference/endpoint/tests-create POST /tests Create a new test Create a new test by providing details such as name, rate, servers, users, host\_id, region, and locustfile. This endpoint allows you to programmatically set up load tests with your specific configuration. # Delete Test Source: https://docs.loadforge.com/api-reference/endpoint/tests-delete DELETE /tests/{test_id} Delete an existing test Delete an existing test by specifying the test\_id. This endpoint allows you to remove tests that are no longer needed from your account. # List Tests Source: https://docs.loadforge.com/api-reference/endpoint/tests-list GET /tests List all the tests on your account List all the tests on your account, including details such as ID, creation timestamp, name, rate, servers, users, host ID, region, and locustfile. # Update Test Source: https://docs.loadforge.com/api-reference/endpoint/tests-update PATCH /tests/{test_id} Update an existing Test, specified by ID Update an existing test, specified by ID. You can modify details such as name, rate, servers, users, host\_id, region, and locustfile to adjust your load testing configuration. # LoadForge REST API Reference Source: https://docs.loadforge.com/api-reference/introduction Complete API documentation for LoadForge load testing — manage hosts, tests, runs, and results programmatically. ## Overview LoadForge provides a comprehensive REST-based API service that enables you to automate your load testing workflows and analyze results programmatically. This documentation covers all available endpoints and their usage. All API requests require authentication using a Bearer token. Create your token in the [User Menu - API](https://app.loadforge.com/user/api-tokens) section. ## Base URL All API requests should be made to: ```text theme={null} https://app.loadforge.com/api/v2 ``` ## Authentication ```bash Authorization Header theme={null} Authorization: Bearer YOUR_TOKEN_HERE ``` ```json OpenAPI Security Schema theme={null} { "security": [ { "bearerAuth": [] } ] } ``` ## Core Endpoints Manage test targets by creating, listing, and deleting host configurations Handle test configurations including creation, updates, and deletion Access detailed test results and performance metrics Control test execution with start and cancel operations ## Model Context Protocol (MCP) Integration ### Overview LoadForge now supports the **Model Context Protocol (MCP)**, enabling seamless integration with other MCP-compatible services for enhanced automation and workflow management. MCP integration allows you to automate test execution, streamline workflows, and enable cross-platform compatibility with other tools in your stack. ### Getting Started with MCP To add LoadForge as an MCP service: ```bash theme={null} npx @mintlify/mcp@latest add loadforge ``` ### Key Benefits * Trigger tests from CI/CD pipelines * Schedule performance monitoring * Set up threshold-based testing * Centralized configuration management * Simplified test scheduling * Consolidated logging and reporting * Dynamic test scaling * Multi-location test execution * Integration with monitoring tools ### Example MCP Workflow Here's a sample workflow that demonstrates how to trigger LoadForge tests using MCP: ```json theme={null} { "name": "LoadForge Test Workflow", "steps": [ { "service": "loadforge", "action": "runTest", "parameters": { "testId": "123456", "schedule": "@hourly" } } ] } ``` ### Implementation Steps Run `npx mcp add loadforge` to set up the integration Create automated workflows that trigger load tests based on your requirements Track test results and adjust performance parameters as needed By integrating LoadForge with MCP, you can create powerful automated testing workflows that enhance your performance testing strategy. # Cloud Costs Source: https://docs.loadforge.com/cloud/costs Understanding cloud costs on LoadForge. LoadForge utilizes **DigitalOcean** to power its load generation capabilities. By running tests through our platform, you can leverage the **scalability of cloud infrastructure** to simulate real-world traffic scenarios efficiently. By default, LoadForge provides **test minutes**, which allow you to run tests on our managed cloud infrastructure. However, you also have the option to **bring your own DigitalOcean account** for cost savings and additional flexibility. ## DigitalOcean Integration and Costs If you choose to integrate your own **DigitalOcean account**, LoadForge will launch its test servers directly within your cloud environment. This means: * You are responsible for **all fees** associated with running the cloud instances. * Costs depend on **test duration and the number of workers required**. Typical pricing per test: * **Small tests (10,000 users)**: \~\$0.05 - \$0.10 per test. * **Larger tests (50,000+ users)**: \~\$0.40 - \$1 per test. * **High-scale tests (100,000+ users)**: \~\$1 - \$3 per test. **Don’t have a DigitalOcean account?** No worries! You can still run tests using LoadForge’s managed cloud infrastructure. However, this will incur a slightly higher charge per test. ## Understanding Test Costs LoadForge ensures full **cost transparency** before each test: * **Cost estimates** are provided before you start a test. * **VM pricing** varies based on the number of users and test workers deployed. * DigitalOcean charges by the **hour**, meaning: * You are billed for a **full hour per worker**, even if the test runs for a shorter duration. * A **controller is always launched**, so your total cost includes **1 controller + number of workers**. When using LoadForge's managed cloud, billing is calculated **per minute** — you only pay for the time you actually use. **Worker Capacity** Each worker can handle **10,000 - 20,000 virtual users** efficiently. ## Sample Cost Estimations For a clearer understanding, here are sample costs based on **user load**: ```text theme={null} ~10,000 virtual users = $0.11 ~70,000 virtual users = $0.66 ~200,000 virtual users = $3.57 ``` While smaller tests are highly cost-effective, larger-scale tests require more resources. LoadForge is still significantly **cheaper than many competitors**, ensuring affordability at all scales. ## Using LoadForge’s Managed Cloud Infrastructure If you choose **not to integrate** your own DigitalOcean account, LoadForge provides an **easy-to-use cloud solution**: * **\$1 per server per hour**, billed by the minute. * **Billing is calculated per minute** — you only pay for the time you actually use. * **No additional setup required**—simply run your test and let LoadForge handle the infrastructure. **Example**: Running a **30-minute test** using **2 servers** on LoadForge’s cloud will cost **\$1** (half the hourly rate). ## Choosing the Right Option | Feature | Bring Your Own Cloud | Use LoadForge’s Cloud | | ------------------- | ---------------------- | --------------------------------------- | | **Setup Required** | Yes | No | | **Cost Efficiency** | Lower per test | Flat rate per minute | | **Flexibility** | Full control | Managed for you | | **Billing Model** | \$0.11 per server/hour | \$1 per server/hour (billed per minute) | We recommend **starting with LoadForge’s cloud** for simplicity. Once comfortable, you can switch to your own **DigitalOcean account** for **cost savings** and **greater control**. Before launching any test, LoadForge provides a **clear cost breakdown**, ensuring that you **never incur unexpected charges**. # Using the Cloud Source: https://docs.loadforge.com/cloud/overview Learn how LoadForge uses the cloud to scale up large numbers of users. ## Cloud Infrastructure Overview LoadForge leverages cloud technology to scale up large numbers of users for load testing. Our dynamic system enables rapid deployment and cleanup of cloud instances, ensuring efficient resource utilization. ### Test Minutes and Options Use LoadForge's default test minutes on our managed cloud infrastructure for hassle-free testing. Bring your own cloud account for enhanced control and potential cost savings. ### Key Benefits Our cloud-based approach delivers three primary advantages: Each benefit is carefully designed to provide maximum value while maintaining environmental responsibility. * Starting from \$0.05 per 10,000 users per test * Transparent pricing structure * Pay only for what you use * Generate millions of SSL transactions per second * Support for massive concurrent users * Real-time scaling capabilities * Resources used only when needed * Efficient resource allocation * Minimal carbon footprint LoadForge proudly contributes **1% of all revenue** (not just profit) to fighting climate change, making us "The Green Load Test Company." ## Cloud Infrastructure Process ### Deployment Workflow When you initiate a test, LoadForge executes a streamlined process: 1. **Provisioning**: Launch brand-new virtual machines 2. **Configuration**: Install all necessary dependencies 3. **Execution**: Run your load test 4. **Cleanup**: Securely wipe data and destroy instances The entire setup process takes **approximately 1 minute**, even when deploying **20+ test workers**. ### Data Security After test completion: * All test data is wiped from cloud instances * Instances are completely destroyed * Results are securely stored in LoadForge's cloud ## Custom Cloud Integration ### Using Your DigitalOcean Account While LoadForge provides managed infrastructure, you can integrate your own DigitalOcean account for enhanced control. 1. **Cost Optimization** * \~\$0.11 per test per hour with your account * Compared to \$1 per hour (billed per minute) on managed cloud * Direct control over resource allocation 2. **Enhanced Flexibility** * Custom instance types * Personalized networking settings * Advanced configuration options 3. **Scalability Control** * Manage worker allocation * Custom resource distribution * Direct infrastructure access 1. Provide your DigitalOcean API key 2. Configure desired settings 3. Start running tests on your infrastructure For newcomers, we recommend starting with LoadForge's managed cloud to simplify the initial setup process. You can always switch to your own cloud account later. ## Enterprise-Scale Testing ### Performance Capabilities LoadForge is engineered for high-scale testing scenarios: * Launch up to 20 servers per test * Run multiple concurrent tests * Support for distributed testing * 10,000 - 20,000 virtual users per worker * Multiple requests per second per user * Billions of requests per hour capability ### Performance Metrics Our infrastructure delivers impressive performance: ```text theme={null} Maximum Performance Capabilities: - 3.2 million transactions per second (TPS) - 11.52 billion SSL requests per hour - Cost: $19.20 per hour for maximum capacity ``` For detailed information about running large-scale tests, refer to our comprehensive guide on [high-scale testing](tests-large-scale). See detailed pricing for managed and self-hosted cloud options Create your free account and start load testing today # Host Verification Source: https://docs.loadforge.com/essentials/hosts About verifying Hosts on LoadForge LoadForge requires that you add and verify hosts before you can run tests against them. This is to prevent users from running tests against websites or services that they do not own. The process is very quick and requires either a simple meta tag or a file placed in your application's public directory. **But why???** We do this to prevent LoadForge from being used to DoS other websites. It helps prove you own the website or application. ## Meta Tag The meta tag we use is called `loadforge-site-verification`, and it needs to be placed in the header of your website or application. For APIs, this is often not appropriate, and the File method is recommended. The final meta tag looks like this (with your unique verification code): ```html example.html theme={null} ``` ## File Verification The file check sends a request to your webserver for the file `loadforge.txt`. If your site is `https://test.com`, then you would need to ensure that `https://test.com/loadforge.txt` is accessible. Once this file is returned with the verification string we provide, the test will pass. The file must contain your verification code, the same as seen in the meta tag example. The code for the meta tag and the file is always identical. ## DNS Verification You may also verify your ownership with a DNS TXT record called `loadforge-site-verification`. It must exist under the host you are trying to validate. For example, `loadforge-site-verification.loadforge.com` must contain your account validation key as the value. ### Lookup Example ```shell nslookup theme={null} ❯ nslookup > set type=txt > loadforge-site-verification.loadforge.com [..] Non-authoritative answer: loadforge-site-verification.loadforge.com text = "8ee17273640295c9[..]" ``` ```shell dig theme={null} ❯ dig loadforge-site-verification.loadforge.com TXT > loadforge-site-verification.loadforge.com [..] loadforge-site-verification.loadforge.com. 300 IN TXT "8ee172736402[..]" ``` ## Automatic Validation We use the same verification tag for your account, regardless of the host. This means you can always use the meta tag or file we provide on any of your applications. This also simplifies adding hosts through the API, as you will know their validation code in advance. Additionally, if you validate `example.com`, we will automatically validate any subdomains under that domain, such as `dev.example.com`, when you add them. ## Alternative Validation If you need to validate your domain manually, you can [contact us](https://loadforge.com/contact) or email [support@loadforge.com](mailto:support@loadforge.com). You will be required to prove ownership. # Simulating Real Users Source: https://docs.loadforge.com/essentials/simulating-real-users Learn how to simulate real user behavior in load testing. When running load tests, you may want to test different types of load. For example, you may want to stress test your web server or load test your API. However, the most common scenario is simulating real users for when you experience a traffic spike. Real user browsing is made up of a few key components: * Simulated wait times * Full page loads * User sessions ## Wait Times Real users wait between page loads—they consume the content, let animations or videos play, and interact with the page. Because of this, you need to simulate a realistic wait time in your test, which depends on the type of site or content. Recommended wait times: * **Slow** - 15 to 60 seconds * **Casual** - 8 to 20 seconds * **Standard** - 5 to 9 seconds You'll notice these are always a range, and LoadForge supports that. Your test will have a `wait_time` variable, which typically looks like this: ```python theme={null} wait_time = between(5, 9) ``` This instructs LoadForge to wait a random amount between 5 and 9 seconds between actions. ## Loading Static Content A real user also loads all your static content—images, CSS, JavaScript, etc. However, browsers cache this content, meaning they load it less frequently as users continue browsing. You want to simulate loading static content first and then focus on dynamic content. Here's an example of a test script that simulates a real user visiting a blog: ```python theme={null} from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(5, 9) @task(3) def index_page(self): self.client.get("/") # Load CSS self.client.get("/assets/css/app.css?v=5182f1a68d") self.client.get("/assets/css/home.css?v=5182f1a68d") self.client.get("/public/cards.min.css?v=5182f1a68d") # Load JavaScript self.client.get("/public/cards.min.js?v=5182f1a68d") self.client.get("/assets/js/manifest.js?v=5182f1a68d") self.client.get("/assets/js/vendor.js?v=5182f1a68d") self.client.get("/assets/js/app.js?v=5182f1a68d") # Simulate visiting different pages @task(1) def visit_pages(self): urls = [ "", "/", "/products/", "/we-care/", "/corporate/", "/products", "/1000-screenshots-per-month-for-free/", "/author/gbv/", "/loadforge-launches-white-labelling-for-reports/", "/tag/loadforge/", "/sponsoring-laravel-io/", "/tag/news/" ] for url in urls: self.client.get(url) ``` In this script: * The `index_page` task loads the homepage along with its static assets. * The `visit_pages` task simulates navigating through various pages of the site. * The `wait_time` ensures that user behavior remains realistic, with a pause between actions. ## Next Steps Simulating real user behavior helps you create realistic load tests, ensuring that your site remains stable under peak traffic conditions. Browse ready-to-use test scripts for common scenarios Record your browser activity and convert it into a load test Configure ramp-up, spike, and step patterns for your tests Step-by-step guide to creating your first test # Test Minutes & Consumption Source: https://docs.loadforge.com/essentials/test-minutes How do test minutes work, and what if you run out? LoadForge does not limit the number of test runs or the number of virtual users on client accounts. However, running load tests incurs infrastructure costs. Specifically, for every worker launched, a per-minute fee is charged by our cloud provider. We call this a **test minute** or **test credit**. ## How Test Minutes Work Each test you run consumes test minutes based on: * The **number of virtual users** simulated. * The **duration** of the test. * The **number of workers** required to execute the load. Test minutes are directly linked to the number of workers used and the duration of your test. LoadForge charges **per minute for the controller** and **per minute for each worker** — you only pay for the time you actually use. ## Managing Your Test Minutes LoadForge provides users with an allocation of test minutes based on their plan. Test minutes **reset on the 1st of each month**. If you find that you need more test minutes, there are several options: * **Optimize test configurations**: Use smaller test runs to fine-tune parameters before running large-scale simulations. * **Upgrade your plan**: Higher plans include more test minutes to accommodate larger testing needs. * **Pay for additional test minutes**: If you exceed your allocation, you can purchase extra minutes on demand. **What if I run out?** Don't worry! If you run out of test minutes, you can pay per test. LoadForge will display the number of credits required and the price before running a test. Typically, the cost is between \$1-2 per test. ## Estimating Test Consumption To help users manage their consumption, LoadForge provides real-time estimations before a test is executed. When you click **Run**, you will see exactly how many test minutes will be consumed before confirming the test execution. Factors affecting test consumption include: * **User concurrency**: More virtual users consume more test minutes. * **Test duration**: Longer tests use more resources. * **Complexity**: Tests that require heavy computation or dynamic requests may need more processing power. ### Best Practices for Reducing Usage The most effective way to reduce test minute consumption is to **only run the workers you need**. LoadForge recommends **1 worker per 10,000 Virtual Users (VUs)**, so avoid over-supplying workers to optimize efficiency. Since billing is per-minute, shorter tests now cost proportionally less — a 5-minute test costs roughly 1/12th of a 60-minute test. Take advantage of this by running quick smoke tests and only scaling up duration when needed. By reviewing the estimated cost before running a test, you can make informed decisions to optimize your testing strategy while minimizing costs. ## Need More Test Minutes? If you require additional test minutes beyond your plan's allocation, LoadForge offers **on-demand test minute purchases**. You can also reach out to our support team for custom solutions tailored to your testing needs. # Understanding Load Testing Source: https://docs.loadforge.com/essentials/understanding-load-testing Learn what load testing is, why it matters, when to run load tests, and how to interpret results. A complete guide for beginners and experienced developers. ## What is Load Testing? Load testing is a method to measure your website or application's performance under different conditions. It helps you understand how your system behaves under both normal and peak load conditions. The primary goal is to determine how many concurrent users can access your site while maintaining optimal speed for end users. Load testing reveals potential bottlenecks before they impact your users in production. This proactive approach helps maintain service quality and user satisfaction. ## Why Load Testing Matters Load testing helps identify critical issues that could impact your service: * System crashes under heavy load * Performance degradation * Scalability limitations * Resource bottlenecks **Prevent Downtime Before It Happens**: Load testing helps you identify and fix performance issues before they affect your users. Don't wait for your service to become popular to discover these problems! ## When to Perform Load Tests ### Pre-Launch Testing The ideal time to conduct load tests is before launching your site to the public. This gives you time to: * Identify potential issues * Make necessary improvements * Validate system performance * Ensure scalability ### Ongoing Testing Regular load testing should be performed: * After major updates * Before expected traffic spikes * During performance optimization * When scaling infrastructure ## Testing Web Applications vs Servers ### Web Application Testing Testing web applications requires a comprehensive approach that covers the full stack. Key areas to test include: #### Dynamic Operations * User authentication * Search functionality * CRUD operations (Create, Read, Update, Delete) * Database interactions #### Content Types 1. Static content (images, CSS, JavaScript) 2. Dynamic content (user profiles, generated pages) 3. Database operations Most modern web applications see bottlenecks in database operations and dynamic content generation. Focus your testing efforts on these areas first. ### Web Server Testing Server testing focuses on raw throughput and requests per second. When testing web servers: #### Remove External Factors * Eliminate database queries * Avoid dynamic content * Disconnect external services #### Test Structure 1. Request small static files (e.g., robots.txt) 2. Test larger image files 3. Optionally test dynamic pages Run separate tests for each content type to accurately measure performance differences between file sizes and content types. ## Understanding Test Results Your load test will generate comprehensive data. Focus on these key metrics: ### Critical Metrics 1. **Average Response Time** * Measures typical page load duration * Indicates overall system performance 2. **Percentile Times (P95/P99)** * Shows performance for slowest 5% and 1% of requests * Helps identify edge case issues 3. **Error Rates** * Tracks system failures * Indicates stability under load 4. **Performance Trends** * Response time patterns * User concurrency impact * Error rate correlation **Focus on P95**: Pay special attention to the 95th percentile response times. If your slowest 5% of requests are problematic, you likely have scalability issues to address. ## Next Steps Run your first load test in under 5 minutes Learn how to create realistic user behavior in your tests Understand how to interpret your test results Browse ready-to-use test scripts for common scenarios Create your LoadForge account and run your first load test in minutes — no credit card required. # Using AI for Scripts Source: https://docs.loadforge.com/essentials/using-ai-models Examples of how to use AI models to create your load tests. OpenAI (ChatGPT), Claude, and Gemini are integrated with LoadForge and locustfiles. This lets you quickly get help with complex load tests and generate locustfiles. ## Example Prompt The following is an example prompt to generate a complex load test case. ```python theme={null} I am using LoadForge and need to create a locustfile that will authenticate against auth0 to get a bearer token, then use that token to communicate with my API. Make example requests for me that will help me test my API. Let’s start with: - GET /api/coins - POST /api/updateCoinPrice Also, when you get api/coins save the json list of coins, and then request - GET /api/coin/{coinId} ``` This prompt returns the following script from ChatGPT, easily and quickly: ```python theme={null} from locust import HttpUser, task, between import json import random import requests AUTH0_DOMAIN = "your-auth0-domain.auth0.com" AUTH0_CLIENT_ID = "your-client-id" AUTH0_CLIENT_SECRET = "your-client-secret" AUTH0_AUDIENCE = "your-api-identifier" USERNAME = "your-auth0-username" PASSWORD = "your-auth0-password" class CoinUser(HttpUser): wait_time = between(1, 3) access_token = None coins = [] def on_start(self): # Auth0 authentication payload = { "grant_type": "password", "username": USERNAME, "password": PASSWORD, "audience": AUTH0_AUDIENCE, "client_id": AUTH0_CLIENT_ID, "client_secret": AUTH0_CLIENT_SECRET, } res = requests.post(f"https://{AUTH0_DOMAIN}/oauth/token", json=payload) res.raise_for_status() self.access_token = res.json()["access_token"] self.client.headers.update({"Authorization": f"Bearer {self.access_token}"}) # Get coins with self.client.get("/api/coins", name="GET /api/coins", catch_response=True) as response: if response.status_code == 200: self.coins = response.json() response.success() else: response.failure("Failed to load coins") @task(2) def get_coin_detail(self): if not self.coins: return coin = random.choice(self.coins) coin_id = coin.get("id") if coin_id: self.client.get(f"/api/coin/{coin_id}", name="GET /api/coin/{coinId}") @task(1) def update_coin_price(self): # Example payload; adjust fields to match your API payload = { "coinId": random.randint(1, 10), "newPrice": round(random.uniform(1, 1000), 2) } self.client.post("/api/updateCoinPrice", json=payload, name="POST /api/updateCoinPrice") ``` # API Versioning Testing Source: https://docs.loadforge.com/examples/advanced-api-patterns/api-versioning Test API versioning strategies including v1 vs v2 endpoints and migration This guide demonstrates how to test API versioning with LoadForge, covering version compatibility, migration scenarios, and deprecation handling. ## Use Cases * Testing backward compatibility between API versions * Validating version-specific functionality and responses * Testing API migration and deprecation workflows * Load testing multiple API versions simultaneously * Validating version negotiation and routing ## Basic API Versioning Testing ```python theme={null} from locust import HttpUser, task, between import json import time import random import uuid class APIVersioningUser(HttpUser): wait_time = between(1, 3) def on_start(self): """Initialize API versioning testing""" self.api_versions = ["v1", "v2", "v3"] self.auth_token = None self.test_data = {} # Authenticate (version-agnostic) self._authenticate() def _authenticate(self): """Authenticate with API (usually version-agnostic)""" auth_data = { "username": f"testuser_{random.randint(1000, 9999)}", "password": "test_password_123" } response = self.client.post('/auth/login', json=auth_data, headers={'Content-Type': 'application/json'}, name="auth_login") if response.status_code == 200: try: auth_result = response.json() self.auth_token = auth_result.get('access_token') except json.JSONDecodeError: pass def _get_headers(self, version=None): """Get headers with optional API version""" headers = {'Content-Type': 'application/json'} if self.auth_token: headers['Authorization'] = f'Bearer {self.auth_token}' if version: # Different versioning strategies headers['API-Version'] = version # Header-based versioning headers['Accept'] = f'application/vnd.api+json;version={version}' # Accept header return headers @task(4) def test_version_header_strategy(self): """Test header-based API versioning""" version = random.choice(self.api_versions) # Create user with specific API version user_data = { "name": f"Test User {random.randint(100, 999)}", "email": f"test{random.randint(1000, 9999)}@example.com", "age": random.randint(18, 80) } # Add version-specific fields if version == "v1": user_data["phone"] = f"+1555{random.randint(1000000, 9999999)}" elif version == "v2": user_data.update({ "phone_number": f"+1555{random.randint(1000000, 9999999)}", "preferences": {"newsletter": True, "sms": False} }) elif version == "v3": user_data.update({ "contact": { "phone": f"+1555{random.randint(1000000, 9999999)}", "preferred_method": "email" }, "settings": { "notifications": {"email": True, "push": True}, "privacy": {"profile_public": False} } }) response = self.client.post('/api/users', json=user_data, headers=self._get_headers(version), name=f"create_user_{version}_header") if response.status_code in [200, 201]: try: created_user = response.json() user_id = created_user.get('id') if user_id: self.test_data[f"user_{version}"] = user_id self._test_version_response_format(version, user_id) except json.JSONDecodeError: pass def _test_version_response_format(self, version, user_id): """Test version-specific response formats""" response = self.client.get(f'/api/users/{user_id}', headers=self._get_headers(version), name=f"get_user_{version}_format") if response.status_code == 200: try: user_data = response.json() # Validate version-specific response structure if version == "v1": # v1 might have flat structure assert 'phone' in user_data, f"v1 should have 'phone' field" elif version == "v2": # v2 might have phone_number instead of phone assert 'phone_number' in user_data, f"v2 should have 'phone_number' field" assert 'preferences' in user_data, f"v2 should have 'preferences' field" elif version == "v3": # v3 might have nested contact structure assert 'contact' in user_data, f"v3 should have 'contact' field" assert 'settings' in user_data, f"v3 should have 'settings' field" except (json.JSONDecodeError, AssertionError) as e: print(f"Version {version} response validation failed: {e}") @task(3) def test_url_path_versioning(self): """Test URL path-based API versioning""" version = random.choice(self.api_versions) # Test different endpoint structures per version product_data = { "name": f"Test Product {random.randint(100, 999)}", "price": round(random.uniform(10.00, 999.99), 2), "category": random.choice(["electronics", "clothing", "books"]) } # Version-specific endpoint paths if version == "v1": endpoint = f'/api/{version}/products' product_data["description"] = "Simple description" elif version == "v2": endpoint = f'/api/{version}/products' product_data.update({ "description": "Enhanced description", "tags": ["test", "product"], "metadata": {"source": "api_test"} }) elif version == "v3": endpoint = f'/api/{version}/catalog/products' # Different path structure product_data.update({ "description": { "short": "Brief description", "long": "Detailed product description" }, "taxonomy": { "category": product_data["category"], "subcategory": "test_subcategory" }, "attributes": {"color": "blue", "size": "medium"} }) response = self.client.post(endpoint, json=product_data, headers=self._get_headers(), name=f"create_product_{version}_path") @task(2) def test_version_compatibility(self): """Test backward compatibility between versions""" # Create data with v1 API v1_data = { "title": f"V1 Article {random.randint(100, 999)}", "content": "Article created with v1 API", "author": "Test Author", "published": True } v1_response = self.client.post('/api/v1/articles', json=v1_data, headers=self._get_headers(), name="create_article_v1") if v1_response.status_code in [200, 201]: try: v1_article = v1_response.json() article_id = v1_article.get('id') if article_id: # Try to access same data with v2 API v2_response = self.client.get(f'/api/v2/articles/{article_id}', headers=self._get_headers("v2"), name="get_article_v1_data_via_v2") # Try to update v1 data with v2 API v2_update = { "title": f"Updated via V2 - {random.randint(100, 999)}", "content": "Updated content via v2 API", "metadata": {"updated_via": "v2", "timestamp": int(time.time())} } self.client.put(f'/api/v2/articles/{article_id}', json=v2_update, headers=self._get_headers("v2"), name="update_v1_article_via_v2") except json.JSONDecodeError: pass @task(1) def test_version_deprecation(self): """Test deprecated version handling""" # Test deprecated v1 endpoint deprecated_data = { "old_field": "value", "legacy_format": True } response = self.client.post('/api/v1/legacy-endpoint', json=deprecated_data, headers=self._get_headers("v1"), name="test_deprecated_v1_endpoint") # Check for deprecation warnings in headers if response.status_code == 200: deprecation_warning = response.headers.get('Deprecation') sunset_header = response.headers.get('Sunset') if deprecation_warning: print(f"Deprecation warning received: {deprecation_warning}") if sunset_header: print(f"Sunset date: {sunset_header}") def on_stop(self): """Cleanup test data across all versions""" for version in self.api_versions: user_id = self.test_data.get(f"user_{version}") if user_id: try: self.client.delete(f'/api/users/{user_id}', headers=self._get_headers(version), name=f"cleanup_user_{version}") except: pass ``` ## Key Testing Points 1. **Version Strategies**: Header, path, query parameter, and Accept header versioning 2. **Backward Compatibility**: Ensure older versions continue to work 3. **Response Formats**: Validate version-specific response structures 4. **Deprecation Handling**: Test deprecated version warnings and sunset dates 5. **Migration Support**: Test data migration between versions 6. **Error Handling**: Proper responses for unsupported versions ``` This guide provides comprehensive API versioning testing patterns for maintaining backward compatibility and smooth version transitions. ``` # Simple Batch Operations Source: https://docs.loadforge.com/examples/advanced-api-patterns/simple-batch-operations Basic batch API testing for endpoints that process multiple items in a single request This guide shows how to test batch/bulk API endpoints that process multiple items at once. Perfect for testing batch create, update, and delete operations. ## Use Cases * Test batch create operations * Test bulk update operations * Test batch delete operations * Validate batch size limits ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json import random class BatchOperationsUser(HttpUser): def on_start(self): self.created_items = [] # Track items we create @task(4) def test_batch_create_users(self): """Test creating multiple users in a single batch request""" # Create batch of 3-5 users batch_size = random.randint(3, 5) users_batch = [] for i in range(batch_size): user = { "name": f"User {random.randint(1000, 9999)}", "email": f"user{random.randint(1000, 9999)}@example.com", "active": True } users_batch.append(user) batch_data = {"users": users_batch} with self.client.post( "/api/users/batch", json=batch_data, name="Batch Create Users" ) as response: if response.status_code in [200, 201]: try: result = response.json() created_count = len(result.get("created", [])) print(f"Batch create: {created_count}/{batch_size} users created") # Store created IDs for later use if "created" in result: for item in result["created"]: if "id" in item: self.created_items.append(item["id"]) except json.JSONDecodeError: print("Batch create successful but no JSON response") else: response.failure(f"Batch create failed: {response.status_code}") @task(3) def test_batch_update_users(self): """Test updating multiple users in a single batch request""" if len(self.created_items) < 2: return # Need at least 2 items to update # Select 2-3 items to update update_count = min(3, len(self.created_items)) items_to_update = random.sample(self.created_items, update_count) updates_batch = [] for item_id in items_to_update: update = { "id": item_id, "name": f"Updated User {random.randint(1000, 9999)}", "active": random.choice([True, False]) } updates_batch.append(update) batch_data = {"updates": updates_batch} with self.client.put( "/api/users/batch", json=batch_data, name="Batch Update Users" ) as response: if response.status_code == 200: try: result = response.json() updated_count = len(result.get("updated", [])) print(f"Batch update: {updated_count}/{update_count} users updated") except json.JSONDecodeError: print("Batch update successful") else: response.failure(f"Batch update failed: {response.status_code}") @task(2) def test_batch_create_products(self): """Test creating multiple products in batch""" batch_size = random.randint(2, 4) products_batch = [] for i in range(batch_size): product = { "name": f"Product {random.randint(1000, 9999)}", "price": round(random.uniform(10.0, 100.0), 2), "category": random.choice(["electronics", "books", "clothing"]) } products_batch.append(product) batch_data = {"products": products_batch} with self.client.post( "/api/products/batch", json=batch_data, name="Batch Create Products" ) as response: if response.status_code in [200, 201]: print(f"Batch created {batch_size} products") else: response.failure(f"Product batch create failed: {response.status_code}") @task(2) def test_large_batch_operation(self): """Test larger batch to check size limits""" # Try a larger batch (10 items) batch_size = 10 large_batch = [] for i in range(batch_size): item = { "name": f"Bulk Item {i}", "value": random.randint(1, 100), "type": "test" } large_batch.append(item) batch_data = {"items": large_batch} with self.client.post( "/api/items/batch", json=batch_data, name="Large Batch Operation" ) as response: if response.status_code in [200, 201]: print(f"Large batch ({batch_size} items) successful") elif response.status_code == 413: print(f"Large batch rejected - payload too large") response.failure("Batch size limit exceeded") elif response.status_code == 400: print(f"Large batch rejected - bad request") response.failure("Large batch validation failed") else: response.failure(f"Large batch failed: {response.status_code}") @task(1) def test_batch_delete(self): """Test deleting multiple items in batch""" if len(self.created_items) < 2: return # Need items to delete # Delete 1-2 items delete_count = min(2, len(self.created_items)) items_to_delete = [] for _ in range(delete_count): if self.created_items: item_id = self.created_items.pop() items_to_delete.append(item_id) if not items_to_delete: return batch_data = {"ids": items_to_delete} with self.client.delete( "/api/users/batch", json=batch_data, name="Batch Delete Users" ) as response: if response.status_code in [200, 204]: print(f"Batch deleted {len(items_to_delete)} users") else: response.failure(f"Batch delete failed: {response.status_code}") @task(1) def test_empty_batch(self): """Test batch operation with empty data""" empty_batch = {"users": []} with self.client.post( "/api/users/batch", json=empty_batch, name="Empty Batch Test" ) as response: if response.status_code == 400: print("Empty batch correctly rejected") elif response.status_code in [200, 201]: print("Empty batch accepted (no-op)") else: response.failure(f"Empty batch unexpected response: {response.status_code}") @task(1) def test_mixed_batch_operation(self): """Test batch with mix of valid and invalid items""" mixed_batch = [ {"name": "Valid User", "email": "valid@example.com"}, {"name": "", "email": "invalid-email"}, # Invalid {"name": "Another Valid User", "email": "valid2@example.com"} ] batch_data = {"users": mixed_batch} with self.client.post( "/api/users/batch", json=batch_data, name="Mixed Batch Test" ) as response: if response.status_code in [200, 201]: try: result = response.json() created = len(result.get("created", [])) errors = len(result.get("errors", [])) print(f"Mixed batch: {created} created, {errors} errors") except json.JSONDecodeError: print("Mixed batch processed") elif response.status_code == 400: print("Mixed batch rejected due to invalid items") else: response.failure(f"Mixed batch failed: {response.status_code}") ``` ## Setup Instructions 1. Replace API endpoints with your actual batch endpoints 2. Adjust batch sizes based on your API limits 3. Customize data structures for your specific entities 4. Update field names to match your API schema ## What This Tests * **Batch Create**: Creating multiple items in one request * **Batch Update**: Updating multiple items in one request * **Batch Delete**: Deleting multiple items in one request * **Size Limits**: Testing batch size limitations * **Error Handling**: Mixed valid/invalid items in batches ## Batch Patterns * **All-or-Nothing**: Entire batch fails if any item fails * **Partial Success**: Some items succeed, others fail * **Best Effort**: Process all valid items, report errors ## Common Batch Endpoints * `POST /api/users/batch` - Create multiple users * `PUT /api/users/batch` - Update multiple users * `DELETE /api/users/batch` - Delete multiple users * `POST /api/items/bulk` - Bulk operations ## Best Practices * Keep batch sizes reasonable (10-100 items) * Handle partial failures gracefully * Provide clear error messages for failed items * Consider batch processing timeouts # Simple Pagination Testing Source: https://docs.loadforge.com/examples/advanced-api-patterns/simple-pagination-testing Basic pagination testing for offset-based, page-based, and cursor-based API pagination This guide shows how to test different pagination patterns in APIs. Perfect for validating pagination logic and performance under load. ## Use Cases * Test pagination performance under load * Validate pagination logic and consistency * Test different pagination patterns * Check boundary conditions and edge cases ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class PaginationTestUser(HttpUser): def on_start(self): # API endpoints with different pagination types self.endpoints = { "/api/users": "offset", # offset-based: ?limit=20&offset=40 "/api/products": "page", # page-based: ?page=2&per_page=25 "/api/orders": "cursor" # cursor-based: ?cursor=abc123&limit=30 } @task(4) def test_offset_pagination(self): """Test offset-based pagination (?limit=20&offset=40)""" endpoint = "/api/users" limit = 20 offset = random.choice([0, 20, 40, 60, 100]) params = {"limit": limit, "offset": offset} with self.client.get( endpoint, params=params, name=f"Offset Pagination - {offset}" ) as response: if response.status_code == 200: try: data = response.json() # Handle different response formats if isinstance(data, dict) and "data" in data: items = data["data"] total = data.get("total", 0) print(f"Offset pagination: {len(items)} items, offset {offset}") # Basic validation if len(items) > limit: response.failure(f"Too many items: {len(items)} > {limit}") else: items = data if isinstance(data, list) else [] print(f"Offset pagination: {len(items)} items") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Pagination failed: {response.status_code}") @task(3) def test_page_pagination(self): """Test page-based pagination (?page=2&per_page=25)""" endpoint = "/api/products" per_page = 25 page = random.choice([1, 2, 3, 4, 5]) params = {"per_page": per_page, "page": page} with self.client.get( endpoint, params=params, name=f"Page Pagination - {page}" ) as response: if response.status_code == 200: try: data = response.json() if isinstance(data, dict): items = data.get("data", data.get("items", [])) current_page = data.get("current_page", data.get("page", page)) total_pages = data.get("total_pages", 0) print(f"Page pagination: {len(items)} items, page {current_page}") # Validate page logic if len(items) > per_page: response.failure(f"Too many items per page: {len(items)}") else: items = data if isinstance(data, list) else [] print(f"Page pagination: {len(items)} items") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Page pagination failed: {response.status_code}") @task(2) def test_cursor_pagination(self): """Test cursor-based pagination (?cursor=abc123&limit=30)""" endpoint = "/api/orders" limit = 30 # Test with different cursors (in real use, get from previous responses) cursor = random.choice([None, "start", "abc123", "xyz789"]) params = {"limit": limit} if cursor: params["cursor"] = cursor with self.client.get( endpoint, params=params, name=f"Cursor Pagination - {cursor or 'start'}" ) as response: if response.status_code == 200: try: data = response.json() if isinstance(data, dict): items = data.get("data", data.get("items", [])) next_cursor = data.get("next_cursor") has_more = data.get("has_more", False) print(f"Cursor pagination: {len(items)} items, next: {next_cursor}") # Basic validation if len(items) > limit: response.failure(f"Too many items: {len(items)} > {limit}") else: items = data if isinstance(data, list) else [] print(f"Cursor pagination: {len(items)} items") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Cursor pagination failed: {response.status_code}") @task(1) def test_pagination_edge_cases(self): """Test pagination edge cases""" endpoint = random.choice(list(self.endpoints.keys())) pagination_type = self.endpoints[endpoint] # Test edge cases based on pagination type if pagination_type == "offset": # Test large offset params = {"limit": 10, "offset": 999999} edge_case = "large_offset" elif pagination_type == "page": # Test page 0 params = {"per_page": 10, "page": 0} edge_case = "page_zero" else: # cursor # Test with empty cursor params = {"limit": 10, "cursor": ""} edge_case = "empty_cursor" with self.client.get( endpoint, params=params, name=f"Edge Case - {edge_case}" ) as response: if response.status_code in [200, 400, 404]: print(f"Edge case {edge_case}: {response.status_code}") else: print(f"Edge case {edge_case}: unexpected {response.status_code}") ``` ## Setup Instructions 1. Replace endpoint URLs with your actual API endpoints 2. Adjust pagination parameters to match your API's format 3. Update field names if your API uses different response structure 4. Customize limit/per\_page values based on your API's defaults ## What This Tests * **Offset Pagination**: Tests limit/offset based pagination * **Page Pagination**: Tests page number and per\_page based pagination * **Cursor Pagination**: Tests cursor-based pagination for large datasets * **Edge Cases**: Tests boundary conditions and invalid parameters ## Best Practices * Test different page sizes to find optimal performance * Validate response structure consistency across pages * Monitor response times for different pagination positions * Test edge cases like empty results and invalid parameters ## Common Issues * **Inconsistent Response Format**: Ensure all pages return same structure * **Performance Degradation**: Later pages may be slower (especially offset-based) * **Boundary Conditions**: Handle edge cases like page 0 or negative offsets * **Cursor Validity**: Cursor-based pagination requires valid cursor tokens # Simple Rate Limiting Source: https://docs.loadforge.com/examples/advanced-api-patterns/simple-rate-limiting Basic rate limiting testing with simple backoff when hitting API limits This guide shows how to handle API rate limits with simple backoff strategies. Perfect for APIs that return 429 status codes when limits are exceeded. ## Use Cases * Test API rate limiting behavior * Handle 429 responses gracefully * Implement basic backoff strategies * Monitor rate limit headers ## Simple Implementation ```python theme={null} from locust import task, HttpUser import time import random class RateLimitUser(HttpUser): def on_start(self): self.backoff_until = 0 # Track when to resume requests self.rate_limit_count = 0 @task(5) def test_api_with_rate_limits(self): """Test API endpoint that has rate limits""" # Check if we're in backoff period if time.time() < self.backoff_until: print("Skipping request - in backoff period") return with self.client.get("/api/data", name="Rate Limited API") as response: if response.status_code == 200: print("Request successful") # Check rate limit headers remaining = response.headers.get("X-RateLimit-Remaining") if remaining: print(f"Requests remaining: {remaining}") # Warn if getting close to limit if int(remaining) < 5: print("WARNING: Approaching rate limit!") elif response.status_code == 429: # Rate limited - implement backoff print("Rate limited! Backing off...") self.rate_limit_count += 1 # Get retry-after header or use default retry_after = response.headers.get("Retry-After", "60") backoff_time = int(retry_after) # Set backoff period self.backoff_until = time.time() + backoff_time print(f"Backing off for {backoff_time} seconds") response.failure(f"Rate limited - backing off for {backoff_time}s") else: response.failure(f"Request failed: {response.status_code}") @task(2) def test_search_api(self): """Test search API with rate limits""" if time.time() < self.backoff_until: return search_terms = ["test", "product", "user", "data"] query = random.choice(search_terms) with self.client.get(f"/api/search?q={query}", name="Search API") as response: if response.status_code == 200: print(f"Search for '{query}' successful") elif response.status_code == 429: print(f"Search rate limited") # Simple backoff self.backoff_until = time.time() + 30 # 30 second backoff response.failure("Search rate limited") else: response.failure(f"Search failed: {response.status_code}") @task(1) def check_rate_limit_status(self): """Check current rate limit status""" if time.time() < self.backoff_until: remaining_backoff = self.backoff_until - time.time() print(f"In backoff for {remaining_backoff:.0f} more seconds") else: print("No active backoff - ready for requests") if self.rate_limit_count > 0: print(f"Total rate limits hit: {self.rate_limit_count}") ``` ## Setup Instructions 1. Replace `/api/data` and `/api/search` with your actual API endpoints 2. Adjust backoff times based on your API's requirements 3. Customize rate limit header names for your API 4. Test with different request volumes to trigger rate limits ## What This Tests * **Rate Limit Detection**: Handles 429 status codes * **Backoff Strategy**: Waits before retrying after rate limits * **Header Monitoring**: Checks remaining request counts * **Recovery**: Resumes requests after backoff period ## Rate Limit Headers Common headers to check: * `X-RateLimit-Remaining`: Requests left in current window * `Retry-After`: Seconds to wait before retrying * `X-RateLimit-Reset`: When the limit resets ## Simple Backoff Strategy When rate limited (429 response): 1. Stop making requests 2. Wait for the time specified in `Retry-After` header 3. Resume requests after backoff period 4. Track how many times you hit limits ## Best Practices * Always respect the `Retry-After` header * Monitor remaining requests to avoid hitting limits * Use exponential backoff for repeated rate limits * Don't retry immediately after being rate limited # Load Testing GraphQL APIs Source: https://docs.loadforge.com/examples/advanced-api-testing/graphql-api A guide on how to load test a GraphQL based API, featuring the SpaceX API as an illustrative example. ## Overview LoadForge makes testing GraphQL APIs straightforward by utilizing standard HTTP POST requests to send GraphQL queries. This guide demonstrates how to create a load test to query the missions from the publicly available SpaceX GraphQL API. The examples provided here can be adapted to test any GraphQL API, including authentication scenarios. *** ## Basic Example Here's a fundamental example showing a standard GraphQL query that requests the mission IDs and names from the SpaceX API: ```graphql theme={null} { missions { id name } } ``` The following example shows how to implement this query for use in a LoadForge test, demonstrating the straightforward integration of GraphQL queries. ```python theme={null} import time from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(3, 5) @task(1) def index_page(self): query = ''' { missions { id name } } ''' response = self.client.post( "/graphql", name="GraphQL", json={"query": query } ) ``` *** ## Authenticating GraphQL Requests Many GraphQL APIs require authentication. The following example shows how to set custom headers for authentication: ```python theme={null} response = self.client.post( "/graphql", name="GraphQL", headers={ "Accept": "application/graphql", "Authorization": "" }, json={"query": query} ) ``` For dynamic authorization token retrieval, the `on_start` method can automate this process, ensuring all requests include the required authentication header: ```python theme={null} def on_start(self): # Send a login request response = requests.post("http://mysite.com/login", {"username": "user", "password": "pass"}) # Extract the "token" from the response header self.client.headers.update({'Authorization': response.headers.get('token')}) ``` *** ## Locust Test Script (locust.py) ```python theme={null} # locust.py import time from locust import HttpUser, between, task class GraphQLUser(HttpUser): wait_time = between(1, 3) host = "https://api.spacex.land/graphql" def on_start(self): # Optionally disable SSL verification self.client.verify = False @task def fetch_missions(self): query = """ { missions { id name } } """ self.client.post( "/", name="GraphQL Query", json={"query": query} ) ``` *** LoadForge is built upon the robust foundation of Locust, which means that if you're already a user of the open-source Locust tool, you can directly use this script in your tests. If you're considering upgrading your testing capabilities, think about importing your Locust script to [LoadForge](https://loadforge.com) and make the most out of your load testing efforts! # Load Testing GraphQL Subscriptions Source: https://docs.loadforge.com/examples/advanced-api-testing/graphql-subscriptions Guide on load testing real-time GraphQL subscriptions with LoadForge using Locust. ## Overview GraphQL subscriptions provide real-time data streams over WebSockets. LoadForge supports subscription load testing by leveraging Locust and the `WebSocketUser` from `locust-plugins`. ## Locust Test Script (locust.py) ```python theme={null} # locust.py import json from locust import between, task from locust_plugins.users import WebSocketUser class GraphqlSubsUser(WebSocketUser): wait_time = between(1, 3) host = "wss://api.yourdomain.com/graphql" def on_start(self): # Open WebSocket connection self.connect("/graphql") # Initialize the subscription init_msg = {"type": "connection_init", "payload": {}} self.send(json.dumps(init_msg)) self.receive() # Start subscription sub_msg = { "id": "1", "type": "start", "payload": {"query": "subscription { newMessage { content sender } }"} } self.send(json.dumps(sub_msg)) @task def listen(self): # Receive a subscription event event = self.receive() data = json.loads(event) print(data) ``` *** **Notes:** * Install dependencies: `pip install locust locust-plugins websockets` if you wish to test locally. * Replace `host` and GraphQL query with your subscription endpoint. # Load Testing gRPC Services Source: https://docs.loadforge.com/examples/advanced-api-testing/grpc Guide on load testing gRPC APIs with LoadForge using Locust. ## Overview Load testing gRPC services requires specialized clients that can serialize and send Protocol Buffer messages. With Locust and the `GrpcUser` from `locust-plugins`, you can simulate high-throughput gRPC calls and measure performance under load. ## Locust Test Script (locust.py) ```python theme={null} # locust.py import os import sys from grpc_tools import protoc from locust import task, between from locust_plugins.users import GrpcUser # Inline .proto definition proto = '''syntax = "proto3"; package helloworld; message HelloRequest { string name = 1; } message HelloReply { string message = 1; } service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } ''' # Write & compile stubs at runtime with open('helloworld.proto', 'w') as f: f.write(proto) protoc.main(['', '--proto_path=.', '--python_out=.', '--grpc_python_out=.', 'helloworld.proto']) # Ensure current dir is importable sys.path.insert(0, os.getcwd()) import helloworld_pb2 class HelloWorldUser(GrpcUser): host = "grpc.server.com:50051" wait_time = between(1, 3) @task def say_hello(self): # Prepare request request = helloworld_pb2.HelloRequest(name="LoadForge") # Call the service self.call(request, "/helloworld.Greeter/SayHello") ``` *** **Notes:** * Install dependencies: `locust-plugins`, `grpcio`, and `grpcio-tools` if you wish to test locally. # Load Testing gRPC Streaming Services Source: https://docs.loadforge.com/examples/advanced-api-testing/grpc-streaming Guide on load testing gRPC streaming APIs (client, server, bidirectional) with LoadForge using Locust. ## Overview gRPC supports streaming RPCs—client-side, server-side, and bidirectional streams—over HTTP/2. Locust with `GrpcUser` can simulate these streaming calls to measure performance under load. ## Locust Test Script (locust.py) ```python theme={null} # locust.py from locust import task, between from locust_plugins.users import GrpcUser import streaming_pb2 class StreamingUser(GrpcUser): host = "grpc.server.com:50051" wait_time = between(1, 2) @task def client_stream(self): requests = (streaming_pb2.StreamRequest(value=i) for i in range(10)) response = self.call(requests, "/streaming.StreamService/ClientStream") _ = response.value @task def server_stream(self): request = streaming_pb2.Empty() responses = self.call(request, "/streaming.StreamService/ServerStream") for msg in responses: _ = msg.value @task def bidi_stream(self): requests = (streaming_pb2.StreamRequest(value=i) for i in range(5)) responses = self.call(requests, "/streaming.StreamService/BidiStream") for msg in responses: _ = msg.value ``` *** **Notes:** * Generate gRPC code via `protoc` with streaming definitions. * Install dependencies: `locust-plugins`, `grpcio`, and `grpcio-tools` if you wish to test locally. # Load Testing JSON‑API Chaining Source: https://docs.loadforge.com/examples/advanced-api-testing/json-api-chaining Guide on chaining dependent JSON API calls using LoadForge and Locust. ## Overview Complex workflows often require multiple API calls: fetch a list of resources, pick random IDs, then retrieve or modify them. This guide shows you how to parse JSON responses and chain HTTP calls in a single Locust script. ## Locust Test Script (locust.py) ```python theme={null} # locust.py import random from locust import HttpUser, between, task class JSONAPIUser(HttpUser): wait_time = between(1, 3) host = "https://api.example.com" def fetch_resources(self): response = self.client.get("/api/items", name="Get Items List") self.item_ids = [item['id'] for item in response.json()] def on_start(self): # Warm up: fetch initial resource list self.fetch_resources() @task(3) def read_item(self): # Randomly pick an existing item item_id = random.choice(self.item_ids) self.client.get(f"/api/items/{item_id}", name="Get Item Detail") @task(1) def create_and_delete(self): # Create a new item response = self.client.post( "/api/items", name="Create Item", json={"name": "LoadForge Item"} ) new_id = response.json().get("id") if new_id: # Clean up by deleting it self.client.delete( f"/api/items/{new_id}", name="Delete Item" ) # Refresh list occasionally self.fetch_resources() ``` *** **Notes:** * Install dependencies: `pip install locust`. * Adjust endpoint paths/names to suit your API schema. * Use `self.client.headers.update(...)` for auth headers if needed. # XML-RPC Load Testing Source: https://docs.loadforge.com/examples/advanced-api-testing/xmlrpc-api Use LoadForge to seamlessly test XML-RPC servers using the powerful capabilities of Python. ## Overview By default, LoadForge is set to test plain HTTP/HTTPS websites. But its versatility allows users to venture beyond this norm. If you're looking to load test an XML-RPC server, LoadForge combined with Python provides a simple and effective way. This documentation walks you through how to achieve that. ## Key Concepts **XML-RPC** stands for "XML Remote Procedure Call". It's a protocol that encodes its calls as XML and transmits via HTTP. It allows for data to be passed, processed, and returned. **LoadForge** is a platform that offers a straightforward way to create load tests using Python scripts, such as those that define interactions with XML-RPC servers. ## Testing XML-RPC Servers Below is a practical example of how you can customize LoadForge to test an XML-RPC server. This script simulates calls to the `get_time()` and `get_random_number()` functions. Replace these calls with those relevant to your XML-RPC service. ### Code Example: ```python theme={null} import time from xmlrpc.client import ServerProxy, Fault from locust import User, task class XmlRpcClient(ServerProxy): """ XmlRpcClient is a tailored version of the standard library's ServerProxy. It captures function calls and triggers the *request* event upon completion, ensuring the calls are logged. """ def __init__(self, host, request_event): super().__init__(host) self._request_event = request_event def __getattr__(self, name): func = ServerProxy.__getattr__(self, name) def wrapper(*args, **kwargs): start_time = time.perf_counter() request_meta = { "request_type": "xmlrpc", "name": name, "response_length": 0, "response": None, "context": {}, # Refer to HttpUser for context implementation details "exception": None, } try: request_meta["response"] = func(*args, **kwargs) except Fault as e: request_meta["exception"] = e request_meta["response_time"] = (time.perf_counter() - start_time) * 1000 self._request_event.fire(**request_meta) # Logging the request return request_meta["response"] return wrapper class XmlRpcUser(User): abstract = True # Ensures that this isn't instantiated as an actual user def __init__(self, environment): super().__init__(environment) self.client = XmlRpcClient(self.host, request_event=environment.events.request) # The user class that is instantiated for testing class MyUser(XmlRpcUser): @task def get_time(self): self.client.get_time() @task def get_random_number(self): self.client.get_random_number(0, 100) ``` *** LoadForge is built on the backbone of Locust, making it compatible with open-source Locust scripts. If you're already a Locust user, you can effortlessly port your scripts to [LoadForge](https://loadforge.com) to amplify your testing capabilities. # Simple A/B Testing Source: https://docs.loadforge.com/examples/advanced-testing-patterns/simple-ab-testing Basic A/B testing validation to compare performance between different variants and measure conversion rates This guide shows how to test A/B testing implementations and compare performance between variants. Perfect for validating A/B test setups and measuring variant effectiveness. ## Use Cases * Test A/B testing implementation * Compare variant performance * Measure conversion rates * Validate traffic splitting ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import time class ABTestUser(HttpUser): def on_start(self): # A/B test variants to test self.variants = { "control": { "name": "Control (A)", "param": "variant=control", "header": {"X-Variant": "control"} }, "variant_b": { "name": "Variant B", "param": "variant=b", "header": {"X-Variant": "variant_b"} }, "variant_c": { "name": "Variant C", "param": "variant=c", "header": {"X-Variant": "variant_c"} } } # Track results for each variant self.variant_results = {variant: {"requests": 0, "successes": 0, "conversions": 0, "response_times": []} for variant in self.variants.keys()} # Test pages self.test_pages = [ "/", "/product", "/pricing", "/signup" ] @task(4) def test_variant_assignment(self): """Test that variants are properly assigned""" # Test random variant assignment page = random.choice(self.test_pages) with self.client.get(page, name="Variant Assignment") as response: if response.status_code == 200: # Check if variant is indicated in response variant_indicators = ["variant", "test", "experiment"] response_text = response.text.lower() detected_variant = None for indicator in variant_indicators: if indicator in response_text: detected_variant = "detected" break if detected_variant: print(f"Variant assignment test: Variant detected on {page}") else: print(f"Variant assignment test: No variant detected on {page}") @task(3) def test_control_variant(self): """Test control variant (A)""" self.test_specific_variant("control") @task(3) def test_variant_b(self): """Test variant B""" self.test_specific_variant("variant_b") @task(2) def test_variant_c(self): """Test variant C""" self.test_specific_variant("variant_c") def test_specific_variant(self, variant_key): """Test a specific variant""" if variant_key not in self.variants: return variant = self.variants[variant_key] page = random.choice(self.test_pages) # Add variant parameter or header params = {} headers = {} if "param" in variant: param_parts = variant["param"].split("=") if len(param_parts) == 2: params[param_parts[0]] = param_parts[1] if "header" in variant: headers.update(variant["header"]) start_time = time.time() with self.client.get( page, params=params, headers=headers, name=f"{variant['name']} - {page}" ) as response: response_time = (time.time() - start_time) * 1000 # Track results self.variant_results[variant_key]["requests"] += 1 self.variant_results[variant_key]["response_times"].append(response_time) if response.status_code == 200: self.variant_results[variant_key]["successes"] += 1 print(f"{variant['name']} test {page}: {response_time:.0f}ms") # Check for conversion indicators if self.check_conversion(response, page): self.variant_results[variant_key]["conversions"] += 1 print(f"{variant['name']} conversion detected on {page}") else: print(f"{variant['name']} test {page}: ERROR {response.status_code}") @task(2) def test_variant_consistency(self): """Test that same user gets same variant consistently""" page = random.choice(self.test_pages) variant_key = random.choice(list(self.variants.keys())) variant = self.variants[variant_key] # Make multiple requests with same session/user identifier user_id = f"test_user_{random.randint(1, 100)}" results = [] for i in range(3): # Test 3 consecutive requests params = {"user_id": user_id} # Add variant parameter if "param" in variant: param_parts = variant["param"].split("=") if len(param_parts) == 2: params[param_parts[0]] = param_parts[1] with self.client.get( page, params=params, name=f"Consistency Test - {variant['name']}" ) as response: if response.status_code == 200: # Check if response indicates same variant results.append(response.status_code) else: results.append(None) # Check consistency if all(r == 200 for r in results): print(f"Consistency test {variant['name']}: PASSED - consistent responses") else: print(f"Consistency test {variant['name']}: INCONSISTENT responses") @task(1) def test_traffic_splitting(self): """Test that traffic is split between variants""" page = random.choice(self.test_pages) variant_counts = {variant: 0 for variant in self.variants.keys()} # Make multiple requests to see traffic distribution for i in range(10): # Simulate random user user_id = f"split_test_user_{i}" with self.client.get( page, params={"user_id": user_id}, name="Traffic Split Test" ) as response: if response.status_code == 200: # Try to detect which variant was served # This is simplified - real implementation would check response content detected_variant = random.choice(list(self.variants.keys())) # Simulate detection variant_counts[detected_variant] += 1 # Analyze distribution total_requests = sum(variant_counts.values()) if total_requests > 0: print("Traffic split results:") for variant, count in variant_counts.items(): percentage = (count / total_requests) * 100 print(f" {self.variants[variant]['name']}: {count}/{total_requests} ({percentage:.1f}%)") def check_conversion(self, response, page): """Check if response indicates a conversion""" # Simple conversion detection based on page and response conversion_indicators = { "/signup": ["success", "welcome", "registered"], "/pricing": ["purchase", "checkout", "buy"], "/product": ["add to cart", "added", "cart"] } if page in conversion_indicators: response_text = response.text.lower() for indicator in conversion_indicators[page]: if indicator in response_text: return True return False @task(1) def compare_variant_performance(self): """Compare performance metrics between variants""" print("\n=== A/B Test Performance Comparison ===") for variant_key, variant in self.variants.items(): results = self.variant_results[variant_key] if results["requests"] > 0: success_rate = (results["successes"] / results["requests"]) * 100 conversion_rate = (results["conversions"] / results["requests"]) * 100 avg_response_time = 0 if results["response_times"]: avg_response_time = sum(results["response_times"]) / len(results["response_times"]) print(f"{variant['name']}:") print(f" Requests: {results['requests']}") print(f" Success Rate: {success_rate:.1f}%") print(f" Conversion Rate: {conversion_rate:.1f}%") print(f" Avg Response Time: {avg_response_time:.0f}ms") else: print(f"{variant['name']}: No data yet") print("=" * 40) @task(1) def test_variant_specific_features(self): """Test features specific to certain variants""" # Test that variant-specific features work variant_features = { "variant_b": "/new-feature", "variant_c": "/experimental-page" } for variant_key, feature_path in variant_features.items(): variant = self.variants[variant_key] # Add variant parameter params = {} if "param" in variant: param_parts = variant["param"].split("=") if len(param_parts) == 2: params[param_parts[0]] = param_parts[1] with self.client.get( feature_path, params=params, name=f"Feature Test - {variant['name']}" ) as response: if response.status_code == 200: print(f"Feature test {variant['name']}: Feature available") elif response.status_code == 404: print(f"Feature test {variant['name']}: Feature not found (expected for some variants)") else: print(f"Feature test {variant['name']}: Unexpected response {response.status_code}") ``` ## Setup Instructions 1. Configure your A/B test variants with parameters or headers 2. Set up conversion tracking indicators 3. Define test pages where A/B tests run 4. Customize variant detection logic for your implementation ## What This Tests * **Variant Assignment**: Users get assigned to correct variants * **Traffic Splitting**: Traffic is distributed between variants * **Consistency**: Same user gets same variant across sessions * **Performance**: Response times and success rates per variant * **Conversions**: Conversion rates for each variant ## A/B Test Metrics * **Success Rate**: Percentage of successful requests per variant * **Conversion Rate**: Percentage of users who convert per variant * **Response Time**: Average response time per variant * **Traffic Distribution**: How traffic is split between variants ## Variant Configuration Configure variants with: * **URL Parameters**: `?variant=control` * **HTTP Headers**: `X-Variant: variant_b` * **Cookies**: Session-based variant assignment * **User ID**: Consistent assignment based on user ## Performance Analysis Compare variants on: * **Load Performance**: Response times under load * **Error Rates**: Which variants have more errors * **Conversion Rates**: Which variants convert better * **User Experience**: Overall user experience metrics ## Common Issues * **Inconsistent Assignment**: Users getting different variants * **Uneven Traffic Split**: Poor traffic distribution * **Variant Leakage**: Control users seeing variant features * **Performance Differences**: Variants having different performance # Simple Chaos Testing Source: https://docs.loadforge.com/examples/advanced-testing-patterns/simple-chaos-testing Basic chaos engineering with random failures, timeouts, and error injection to test system resilience This guide shows how to introduce controlled chaos into your load tests to validate system resilience. Perfect for testing how your application handles failures and unexpected conditions. ## Use Cases * Test system resilience under failures * Validate error handling and recovery * Check timeout behavior * Test circuit breaker patterns ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import time class ChaosTestUser(HttpUser): def on_start(self): # Chaos configuration self.chaos_enabled = True self.failure_rate = 0.1 # 10% of requests will have chaos # Test endpoints self.endpoints = [ "/api/users", "/api/products", "/api/orders", "/api/health" ] # Chaos scenarios self.chaos_scenarios = [ "timeout", "slow_response", "random_failure", "bad_request" ] @task(4) def normal_request(self): """Normal request that might have chaos injected""" endpoint = random.choice(self.endpoints) # Decide if we should inject chaos if self.chaos_enabled and random.random() < self.failure_rate: self.inject_chaos(endpoint) else: self.make_normal_request(endpoint) @task(1) def intentional_chaos(self): """Intentionally trigger chaos scenarios""" endpoint = random.choice(self.endpoints) scenario = random.choice(self.chaos_scenarios) print(f"Intentional chaos: {scenario} on {endpoint}") if scenario == "timeout": self.test_timeout_resilience(endpoint) elif scenario == "slow_response": self.test_slow_response(endpoint) elif scenario == "random_failure": self.test_random_failure(endpoint) elif scenario == "bad_request": self.test_bad_request(endpoint) def inject_chaos(self, endpoint): """Randomly inject chaos into a request""" scenario = random.choice(self.chaos_scenarios) print(f"Chaos injection: {scenario} on {endpoint}") if scenario == "timeout": self.test_timeout_resilience(endpoint) elif scenario == "slow_response": self.test_slow_response(endpoint) elif scenario == "random_failure": self.test_random_failure(endpoint) else: self.make_normal_request(endpoint) def make_normal_request(self, endpoint): """Make a normal request without chaos""" with self.client.get(endpoint, name=f"Normal - {endpoint}") as response: if response.status_code == 200: print(f"Normal request to {endpoint}: SUCCESS") else: print(f"Normal request to {endpoint}: {response.status_code}") def test_timeout_resilience(self, endpoint): """Test how system handles timeouts""" try: with self.client.get( endpoint, timeout=0.5, # Very short timeout name=f"Chaos Timeout - {endpoint}" ) as response: if response.status_code == 200: print(f"Timeout test {endpoint}: Surprisingly fast response") else: print(f"Timeout test {endpoint}: {response.status_code}") except Exception as e: print(f"Timeout test {endpoint}: Timeout occurred (expected)") # This is expected behavior - system should handle timeouts gracefully def test_slow_response(self, endpoint): """Test system behavior with artificially slow responses""" # Add artificial delay before request delay = random.uniform(2.0, 5.0) time.sleep(delay) start_time = time.time() with self.client.get(endpoint, name=f"Chaos Slow - {endpoint}") as response: response_time = time.time() - start_time if response.status_code == 200: print(f"Slow response test {endpoint}: {response_time:.2f}s") else: print(f"Slow response test {endpoint}: {response.status_code} in {response_time:.2f}s") def test_random_failure(self, endpoint): """Test random failure scenarios""" # Simulate different types of failures failure_types = [ ("connection_error", self.simulate_connection_error), ("server_error", self.simulate_server_error), ("client_error", self.simulate_client_error) ] failure_type, failure_func = random.choice(failure_types) print(f"Random failure test {endpoint}: {failure_type}") failure_func(endpoint) def simulate_connection_error(self, endpoint): """Simulate connection errors""" try: # Use invalid host to trigger connection error invalid_url = endpoint.replace(self.host, "http://invalid-host-12345.com") with self.client.get( invalid_url, timeout=2.0, name=f"Chaos Connection Error - {endpoint}" ) as response: print(f"Connection error test: Unexpected success") except Exception as e: print(f"Connection error test: Failed as expected ({type(e).__name__})") def simulate_server_error(self, endpoint): """Test how system handles server errors""" # Try to trigger server errors with bad data bad_data = {"invalid": "data" * 1000} # Large payload with self.client.post( endpoint, json=bad_data, name=f"Chaos Server Error - {endpoint}" ) as response: if response.status_code >= 500: print(f"Server error test {endpoint}: Got {response.status_code} (expected)") elif response.status_code >= 400: print(f"Server error test {endpoint}: Got {response.status_code} (client error)") else: print(f"Server error test {endpoint}: Unexpected success {response.status_code}") def simulate_client_error(self, endpoint): """Test client error handling""" # Send malformed requests malformed_scenarios = [ ("invalid_json", '{"invalid": json}'), ("missing_content_type", {"data": "test"}), ("wrong_method", "DELETE") ] scenario, data = random.choice(malformed_scenarios) if scenario == "invalid_json": with self.client.post( endpoint, data=data, # Invalid JSON string headers={"Content-Type": "application/json"}, name=f"Chaos Invalid JSON - {endpoint}" ) as response: print(f"Invalid JSON test {endpoint}: {response.status_code}") elif scenario == "missing_content_type": with self.client.post( endpoint, json=data, headers={"Content-Type": "text/plain"}, # Wrong content type name=f"Chaos Wrong Content-Type - {endpoint}" ) as response: print(f"Wrong content-type test {endpoint}: {response.status_code}") elif scenario == "wrong_method": with self.client.delete( endpoint, name=f"Chaos Wrong Method - {endpoint}" ) as response: print(f"Wrong method test {endpoint}: {response.status_code}") def test_bad_request(self, endpoint): """Test bad request handling""" # Send requests with bad parameters bad_params = { "invalid_param": "value", "limit": -1, # Invalid limit "page": "not_a_number" # Invalid page } with self.client.get( endpoint, params=bad_params, name=f"Chaos Bad Request - {endpoint}" ) as response: if response.status_code == 400: print(f"Bad request test {endpoint}: Properly rejected (400)") elif response.status_code == 200: print(f"Bad request test {endpoint}: Accepted bad params (potential issue)") else: print(f"Bad request test {endpoint}: {response.status_code}") @task(1) def test_system_recovery(self): """Test if system recovers after chaos""" # First, cause some chaos endpoint = random.choice(self.endpoints) self.inject_chaos(endpoint) # Wait a bit time.sleep(1) # Then test if system is still responsive with self.client.get(endpoint, name=f"Recovery Test - {endpoint}") as response: if response.status_code == 200: print(f"Recovery test {endpoint}: System recovered successfully") else: print(f"Recovery test {endpoint}: System still having issues ({response.status_code})") ``` ## Setup Instructions 1. Configure chaos parameters (failure rate, scenarios) 2. Define endpoints you want to test with chaos 3. Adjust timeout values based on your system 4. Monitor system behavior during chaos testing ## What This Tests * **Timeout Handling**: How system responds to request timeouts * **Error Recovery**: System's ability to recover from failures * **Bad Input Handling**: Response to malformed requests * **Connection Failures**: Behavior when connections fail ## Chaos Scenarios * **Timeouts**: Very short request timeouts * **Slow Responses**: Artificially delayed requests * **Connection Errors**: Simulated network failures * **Server Errors**: Requests designed to trigger 5xx errors * **Client Errors**: Malformed requests causing 4xx errors ## Monitoring During Chaos Watch for: * **Error Rates**: Acceptable failure rates during chaos * **Recovery Time**: How quickly system recovers * **Cascading Failures**: Failures spreading to other services * **Resource Usage**: CPU/memory during failure scenarios ## Safety Tips * **Start Small**: Begin with low failure rates * **Monitor Closely**: Watch system metrics during tests * **Have Rollback**: Be ready to stop chaos if needed * **Test in Staging**: Don't run chaos in production initially # Simple Contract Testing Source: https://docs.loadforge.com/examples/advanced-testing-patterns/simple-contract-testing Basic API contract testing with response schema validation and status code checking This guide shows how to test API contracts by validating response schemas and status codes. Perfect for ensuring API consistency and catching breaking changes. ## Use Cases * Validate API response schemas * Check expected status codes * Test required response fields * Ensure API contract compliance ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json class ContractTestUser(HttpUser): def on_start(self): # Expected API contracts self.contracts = { "/api/users": { "status_code": 200, "required_fields": ["id", "name", "email"], "field_types": { "id": int, "name": str, "email": str } }, "/api/products": { "status_code": 200, "required_fields": ["id", "name", "price"], "field_types": { "id": int, "name": str, "price": (int, float) } }, "/api/orders": { "status_code": 200, "required_fields": ["id", "user_id", "total"], "field_types": { "id": int, "user_id": int, "total": (int, float) } } } @task(3) def test_users_contract(self): """Test users API contract""" endpoint = "/api/users" with self.client.get(endpoint, name="Users Contract") as response: self.validate_contract(endpoint, response) @task(2) def test_products_contract(self): """Test products API contract""" endpoint = "/api/products" with self.client.get(endpoint, name="Products Contract") as response: self.validate_contract(endpoint, response) @task(2) def test_orders_contract(self): """Test orders API contract""" endpoint = "/api/orders" with self.client.get(endpoint, name="Orders Contract") as response: self.validate_contract(endpoint, response) @task(1) def test_single_user_contract(self): """Test single user API contract""" endpoint = "/api/users/1" with self.client.get(endpoint, name="Single User Contract") as response: # Use same contract as users list but for single item if response.status_code == 200: try: data = response.json() self.validate_single_item_schema(data, self.contracts["/api/users"]) print("Single user contract: PASSED") except Exception as e: response.failure(f"Single user contract failed: {e}") elif response.status_code == 404: print("Single user contract: User not found (acceptable)") else: response.failure(f"Unexpected status code: {response.status_code}") def validate_contract(self, endpoint, response): """Validate API response against contract""" if endpoint not in self.contracts: response.failure(f"No contract defined for {endpoint}") return contract = self.contracts[endpoint] # Check status code if response.status_code != contract["status_code"]: response.failure(f"Status code mismatch: expected {contract['status_code']}, got {response.status_code}") return # Parse JSON response try: data = response.json() except json.JSONDecodeError: response.failure("Invalid JSON response") return # Handle both single items and arrays if isinstance(data, list): if len(data) > 0: self.validate_single_item_schema(data[0], contract) print(f"Contract validation for {endpoint}: PASSED (array with {len(data)} items)") else: self.validate_single_item_schema(data, contract) print(f"Contract validation for {endpoint}: PASSED") def validate_single_item_schema(self, item, contract): """Validate a single item against the contract schema""" # Check required fields for field in contract["required_fields"]: if field not in item: raise ValueError(f"Missing required field: {field}") # Check field types for field, expected_type in contract["field_types"].items(): if field in item: if not isinstance(item[field], expected_type): raise ValueError(f"Field {field} has wrong type: expected {expected_type}, got {type(item[field])}") @task(1) def test_error_responses(self): """Test error response contracts""" # Test 404 response with self.client.get("/api/users/99999", name="404 Error Contract") as response: if response.status_code == 404: try: error_data = response.json() # Basic error response structure if "error" in error_data or "message" in error_data: print("404 error contract: PASSED") else: response.failure("404 response missing error message") except json.JSONDecodeError: response.failure("404 response not valid JSON") else: response.failure(f"Expected 404, got {response.status_code}") @task(1) def test_post_contract(self): """Test POST request contract""" user_data = { "name": "Test User", "email": "test@example.com" } with self.client.post( "/api/users", json=user_data, name="POST Contract" ) as response: if response.status_code in [201, 200]: try: created_user = response.json() # Validate created user has required fields required_fields = ["id", "name", "email"] for field in required_fields: if field not in created_user: response.failure(f"Created user missing field: {field}") return # Validate the data we sent is reflected if created_user["name"] != user_data["name"]: response.failure("Created user name doesn't match input") return if created_user["email"] != user_data["email"]: response.failure("Created user email doesn't match input") return print("POST contract validation: PASSED") except json.JSONDecodeError: response.failure("POST response not valid JSON") elif response.status_code == 400: # Bad request is acceptable for validation errors print("POST contract: Validation error (acceptable)") else: response.failure(f"Unexpected POST status code: {response.status_code}") ``` ## Setup Instructions 1. Define your API contracts with expected fields and types 2. Add endpoints you want to test to the contracts dictionary 3. Customize field validation rules for your specific API 4. Run tests to catch contract violations ## What This Tests * **Response Schemas**: Validates JSON structure and required fields * **Status Codes**: Ensures endpoints return expected HTTP codes * **Field Types**: Checks that fields have correct data types * **Error Handling**: Tests error response formats ## Contract Validation The guide validates: * **Required Fields**: Ensures critical fields are always present * **Data Types**: Validates field types (string, integer, etc.) * **Status Codes**: Checks HTTP response codes match expectations * **Error Responses**: Validates error message structure ## Common Contract Issues * **Missing Fields**: Required fields not in response * **Wrong Types**: Fields with unexpected data types * **Status Code Changes**: Endpoints returning different codes * **Schema Drift**: API responses changing over time ## Extending Contracts Add more validation rules: * **Field Formats**: Email, date, URL validation * **Value Ranges**: Min/max values for numbers * **Enum Values**: Restricted field values * **Nested Objects**: Complex object validation # Simple Snapshot Testing Source: https://docs.loadforge.com/examples/advanced-testing-patterns/simple-snapshot-testing Basic snapshot testing to compare API responses against saved snapshots for regression detection This guide shows how to capture and compare API response snapshots to detect regressions. Perfect for ensuring API responses remain consistent over time. ## Use Cases * Detect API response changes * Validate response structure consistency * Catch unintended API modifications * Monitor API stability over time ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json import hashlib import time class SnapshotTestUser(HttpUser): def on_start(self): # Endpoints to snapshot self.endpoints = [ "/api/users", "/api/products", "/api/orders", "/api/health" ] # Store snapshots in memory (in real use, save to file/database) self.snapshots = {} self.snapshot_mismatches = [] # Initialize baseline snapshots self.create_baseline_snapshots() def create_baseline_snapshots(self): """Create initial snapshots to compare against""" print("Creating baseline snapshots...") for endpoint in self.endpoints: try: with self.client.get(endpoint, name=f"Baseline - {endpoint}") as response: if response.status_code == 200: snapshot_data = self.create_snapshot(response) self.snapshots[endpoint] = snapshot_data print(f"Baseline snapshot created for {endpoint}") else: print(f"Failed to create baseline for {endpoint}: {response.status_code}") except Exception as e: print(f"Error creating baseline for {endpoint}: {e}") @task(4) def test_api_snapshot(self): """Test API response against saved snapshot""" endpoint = self.random_endpoint() with self.client.get(endpoint, name=f"Snapshot Test - {endpoint}") as response: if response.status_code == 200: current_snapshot = self.create_snapshot(response) self.compare_snapshots(endpoint, current_snapshot) else: response.failure(f"Snapshot test failed: {response.status_code}") @task(2) def test_response_structure(self): """Test that response structure matches snapshot""" endpoint = self.random_endpoint() with self.client.get(endpoint, name=f"Structure Test - {endpoint}") as response: if response.status_code == 200: try: data = response.json() structure = self.extract_structure(data) if endpoint in self.snapshots: baseline_structure = self.snapshots[endpoint].get("structure") if structure == baseline_structure: print(f"Structure test {endpoint}: PASSED") else: print(f"Structure test {endpoint}: FAILED - structure changed") response.failure("Response structure changed") else: print(f"Structure test {endpoint}: No baseline available") except json.JSONDecodeError: response.failure("Invalid JSON response") @task(2) def test_response_fields(self): """Test that required fields are present""" endpoint = self.random_endpoint() with self.client.get(endpoint, name=f"Fields Test - {endpoint}") as response: if response.status_code == 200: try: data = response.json() if endpoint in self.snapshots: baseline_fields = self.snapshots[endpoint].get("fields", set()) current_fields = self.extract_fields(data) missing_fields = baseline_fields - current_fields new_fields = current_fields - baseline_fields if missing_fields: print(f"Fields test {endpoint}: Missing fields: {missing_fields}") response.failure(f"Missing fields: {missing_fields}") elif new_fields: print(f"Fields test {endpoint}: New fields detected: {new_fields}") else: print(f"Fields test {endpoint}: PASSED") else: print(f"Fields test {endpoint}: No baseline available") except json.JSONDecodeError: response.failure("Invalid JSON response") @task(1) def test_response_size(self): """Test that response size is within expected range""" endpoint = self.random_endpoint() with self.client.get(endpoint, name=f"Size Test - {endpoint}") as response: if response.status_code == 200: current_size = len(response.text) if endpoint in self.snapshots: baseline_size = self.snapshots[endpoint].get("size", 0) size_diff = abs(current_size - baseline_size) size_change_percent = (size_diff / baseline_size * 100) if baseline_size > 0 else 0 if size_change_percent > 50: # More than 50% change print(f"Size test {endpoint}: Large size change: {size_change_percent:.1f}%") response.failure(f"Response size changed significantly: {size_change_percent:.1f}%") elif size_change_percent > 20: # 20-50% change print(f"Size test {endpoint}: Moderate size change: {size_change_percent:.1f}%") else: print(f"Size test {endpoint}: Size stable ({size_change_percent:.1f}% change)") else: print(f"Size test {endpoint}: No baseline size available") def create_snapshot(self, response): """Create a snapshot from API response""" try: data = response.json() snapshot = { "timestamp": time.time(), "status_code": response.status_code, "size": len(response.text), "content_hash": hashlib.md5(response.text.encode()).hexdigest(), "structure": self.extract_structure(data), "fields": self.extract_fields(data), "headers": dict(response.headers) } return snapshot except json.JSONDecodeError: return { "timestamp": time.time(), "status_code": response.status_code, "size": len(response.text), "content_hash": hashlib.md5(response.text.encode()).hexdigest(), "error": "Invalid JSON" } def extract_structure(self, data): """Extract the structure of JSON data""" if isinstance(data, dict): return {key: self.extract_structure(value) for key, value in data.items()} elif isinstance(data, list): if data: return [self.extract_structure(data[0])] # Structure of first item else: return [] else: return type(data).__name__ def extract_fields(self, data): """Extract all field names from JSON data""" fields = set() def collect_fields(obj, prefix=""): if isinstance(obj, dict): for key, value in obj.items(): field_name = f"{prefix}.{key}" if prefix else key fields.add(field_name) collect_fields(value, field_name) elif isinstance(obj, list) and obj: collect_fields(obj[0], prefix) # Check first item structure collect_fields(data) return fields def compare_snapshots(self, endpoint, current_snapshot): """Compare current snapshot with baseline""" if endpoint not in self.snapshots: print(f"Snapshot comparison {endpoint}: No baseline snapshot") return baseline = self.snapshots[endpoint] # Compare content hash if current_snapshot["content_hash"] == baseline["content_hash"]: print(f"Snapshot comparison {endpoint}: IDENTICAL") return # Check what changed changes = [] if current_snapshot["status_code"] != baseline["status_code"]: changes.append(f"status_code: {baseline['status_code']} -> {current_snapshot['status_code']}") if current_snapshot["structure"] != baseline["structure"]: changes.append("response structure changed") size_diff = current_snapshot["size"] - baseline["size"] if abs(size_diff) > 100: # More than 100 bytes difference changes.append(f"size: {size_diff:+d} bytes") if changes: print(f"Snapshot comparison {endpoint}: CHANGED - {', '.join(changes)}") self.snapshot_mismatches.append({ "endpoint": endpoint, "changes": changes, "timestamp": current_snapshot["timestamp"] }) else: print(f"Snapshot comparison {endpoint}: Minor changes detected") def random_endpoint(self): """Get a random endpoint for testing""" import random return random.choice(self.endpoints) @task(1) def report_snapshot_status(self): """Report overall snapshot testing status""" total_endpoints = len(self.endpoints) endpoints_with_baselines = len(self.snapshots) total_mismatches = len(self.snapshot_mismatches) print(f"Snapshot Status: {endpoints_with_baselines}/{total_endpoints} baselines, {total_mismatches} mismatches") if total_mismatches > 0: print("Recent mismatches:") for mismatch in self.snapshot_mismatches[-3:]: # Show last 3 print(f" {mismatch['endpoint']}: {', '.join(mismatch['changes'])}") ``` ## Setup Instructions 1. Run initial test to create baseline snapshots 2. Save snapshots to persistent storage (file/database) for real use 3. Configure endpoints you want to monitor 4. Set appropriate thresholds for size and structure changes ## What This Tests * **Response Consistency**: Detects when API responses change * **Structure Stability**: Monitors JSON structure changes * **Field Presence**: Ensures required fields remain present * **Response Size**: Tracks significant size changes ## Snapshot Components * **Content Hash**: MD5 hash of entire response * **Structure**: JSON structure template * **Fields**: All field names in response * **Size**: Response size in bytes * **Headers**: HTTP response headers ## Change Detection The guide detects: * **Structure Changes**: JSON schema modifications * **Missing Fields**: Required fields removed * **New Fields**: Additional fields added * **Size Changes**: Significant response size changes * **Status Code Changes**: HTTP status modifications ## Best Practices * **Regular Baselines**: Update baselines when changes are intentional * **Threshold Tuning**: Adjust change thresholds for your API * **Persistent Storage**: Save snapshots to files or database * **Change Review**: Review all detected changes before deployment ## Common Use Cases * **Regression Testing**: Catch unintended API changes * **API Monitoring**: Continuous API stability monitoring * **Version Validation**: Ensure API versions remain stable * **Documentation Sync**: Verify API matches documentation # Load Testing Authenticated Login Flows Source: https://docs.loadforge.com/examples/authentication-security/auth-login-flow Guide on handling CSRF tokens, sessions, and protected pages with LoadForge using Locust. ## Overview Testing authenticated flows requires obtaining CSRF tokens, logging in with credentials, maintaining session cookies, and accessing protected endpoints. Locust’s `HttpUser` manages cookies automatically, enabling realistic user simulations. ## Locust Test Script (locust.py) ```python theme={null} # locust.py from bs4 import BeautifulSoup from locust import HttpUser, between, task class AuthFlowUser(HttpUser): wait_time = between(1, 3) host = "https://example.com" def on_start(self): # Load login page to fetch CSRF token response = self.client.get("/login", name="Get Login Page") soup = BeautifulSoup(response.text, "html.parser") token = soup.find("input", {"name": "csrf_token"})["value"] # Perform login with CSRF and credentials self.client.post( "/login", name="Perform Login", data={"username": "user", "password": "pass", "csrf_token": token} ) @task(3) def load_dashboard(self): self.client.get("/dashboard", name="Dashboard") @task(1) def load_profile(self): self.client.get("/profile", name="Profile") ``` *** **Notes:** * Install dependencies: `pip install locust beautifulsoup4`. * Ensure form field names (`csrf_token`, `username`, `password`) match your application. # Azure AD Authentication and Testing Source: https://docs.loadforge.com/examples/authentication-security/azure-ad-testing How to authenticate with Azure AD and test your application. Below is a **Locustfile** that demonstrates how to: 1. **Authenticate with Azure AD** and obtain an access token. 2. **Store and reuse cookies** (e.g., `AspNetCore.Antiforgery`, `AspNetCore.Cookies`). 3. **Call endpoints with correct headers** while handling stateful session data. *** ### Example test script for LoadForge This script: * Logs in to Azure AD using client credentials. * Stores and manages cookies and tokens. * Executes requests in a simulated user flow. ```python theme={null} from locust import HttpUser, task, between import requests import json class PortalUser(HttpUser): wait_time = between(1, 3) # Simulated user wait time token_url = "https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/token" client_id = "YOUR_CLIENT_ID" client_secret = "YOUR_CLIENT_SECRET" scope = "https://graph.microsoft.com/.default" token = None headers = {} cookies = {} def on_start(self): """Fetch OAuth2 Token and store it for use in all requests.""" self.token = self.get_oauth_token() self.headers = { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json", } # Initial request to fetch session cookies self.login_portal() def get_oauth_token(self): """Retrieve OAuth token from Azure AD.""" data = { "client_id": self.client_id, "client_secret": self.client_secret, "scope": self.scope, "grant_type": "client_credentials", } response = requests.post(self.token_url, data=data) response.raise_for_status() # Ensure request succeeded return response.json().get("access_token") def login_portal(self): """Initial request to portal to retrieve required cookies.""" response = self.client.get("/login", headers=self.headers) self.cookies.update(response.cookies.get_dict()) # Store cookies @task def perform_task(self): """Perform a simulated user action with correct authentication headers.""" self.headers["X-CSRF-Token"] = self.cookies.get("AspNetCore.Antiforgery", "") response = self.client.get( "/dashboard", headers=self.headers, cookies=self.cookies ) if "AspNetCore.Antiforgery" in response.cookies: self.cookies["AspNetCore.Antiforgery"] = response.cookies["AspNetCore.Antiforgery"] # Simulated POST action (e.g., event signup) self.client.post( "/event/signup", headers=self.headers, cookies=self.cookies, json={"event_id": 123} ) ``` *** ### Key Features 1. **OAuth2 Token Handling** * Retrieves an **access token** from Azure AD. * Uses **client credentials flow** (modify if using user authentication). 2. **Session Management** * Stores **ASP.NET Core antiforgery tokens** and **cookies** dynamically. * Ensures requests include required headers. 3. **User Flow Simulation** * Starts with a **login request** to set up cookies. * Calls `/dashboard` (or another resource) with **authentication headers**. * Simulates an **event signup** to test form submissions. *** ### Modifications for Your Portal * Update **Azure AD settings** (replace `YOUR_TENANT_ID`, `YOUR_CLIENT_ID`, etc.). * Change request **endpoints (`/login`, `/dashboard`, `/event/signup`)** to match your portal. * Modify authentication **grant type** if using **authorization code flow**. # Bypass CloudFlare Blocks Source: https://docs.loadforge.com/examples/authentication-security/bypassing-cloudflare-waf How to prevent CloudFlare from blocking LoadForge with HTTP 403 errors When using LoadForge to test your website's performance, Cloudflare's security measures may inadvertently block or interfere with the testing traffic. This can lead to inaccurate test results and hinder your ability to assess your site's performance under load. To prevent this, you can implement one of the following solutions to allow LoadForge's traffic through Cloudflare without compromising your website's security. ## 1. Temporarily Disable Cloudflare Protection Temporarily lowering or disabling Cloudflare's security settings during your testing period ensures that LoadForge's traffic is not blocked. ### Instructions 1. **Access Your Cloudflare Dashboard**: * Log in to your [Cloudflare dashboard](https://dash.cloudflare.com/) and select the website you want to test. 2. **Lower Security Settings**: * Navigate to the **Security** section. * Go to **WAF** > **Settings**. * Adjust the **Security Level** to **Essentially Off** or **Low**. 3. **Disable Specific Features (Optional)**: * If needed, you can also disable **Bot Fight Mode** or **Rate Limiting** under the **Bots** and **Rate Limiting** sections, respectively. 4. **Run Your LoadForge Tests**: * Perform your load testing while the security settings are reduced. 5. **Revert Security Settings**: * After testing, revert the security settings to their original levels to re-enable full protection. ### Pros * **Quick Implementation**: Easy to execute without advanced configuration. * **Immediate Effect**: Changes take effect promptly, allowing for immediate testing. ### Cons * **Reduced Security**: Temporarily exposes your website to potential threats. * **Manual Reversion Needed**: Requires you to remember to restore security settings afterward. *** ## 2. Whitelist LoadForge IP Ranges By whitelisting LoadForge's IP ranges, you allow its testing traffic while maintaining normal security measures for other visitors. ### Instructions 1. **Obtain LoadForge IP Ranges**: * Visit LoadForge's [IP Ranges page](https://loadforge.com/docs/ip-ranges) to get the list of IP addresses used for testing. 2. **Access Cloudflare Dashboard**: * Log in to your [Cloudflare dashboard](https://dash.cloudflare.com/) and select your website. 3. **Navigate to IP Access Rules**: * Go to **Security** > **WAF** > **Tools**. 4. **Add IP Ranges to Whitelist**: * Under **IP Access Rules**, click **Add**. * Enter each LoadForge IP range. * Set the action to **Allow**. * Choose **This Website** under **Zone** to apply the rule only to your site. 5. **Save Changes**: * Confirm and save each entry to update your firewall rules. ### Pros * **Targeted Allowance**: Only LoadForge's IP addresses are permitted, keeping your site secure from other sources. * **Set and Forget**: Once configured, no need to adjust settings for future tests unless IP ranges change. ### Cons * **Maintenance Required**: LoadForge may update their IP ranges, necessitating updates on your end. * **IP Spoofing Risk**: Though minimal, there's a potential risk if an attacker spoofs a whitelisted IP. *** ## 3. Add and Whitelist a Custom Header Instruct LoadForge to send a custom HTTP header with its requests and configure Cloudflare to allow traffic containing this header. ### Instructions 1. **Set Up Custom Header in LoadForge**: * Log in to your [LoadForge account](https://loadforge.com/login). * Navigate to your test configuration. * Add a custom header (e.g., `X-LoadForge-Test: true`). Refer to LoadForge's [Custom Headers documentation](https://loadforge.com/docs/custom-headers) for detailed steps. 2. **Access Cloudflare Dashboard**: * Log in to your [Cloudflare dashboard](https://dash.cloudflare.com/) and select your website. 3. **Create a WAF Custom Rule**: * Go to **Security** > **WAF** > **Custom Rules**. * Click **Create Rule**. 4. **Configure the Rule**: * **Rule Name**: Enter a descriptive name like "Allow LoadForge Testing". * **When incoming requests match...**: * **Field**: Select **HTTP Request Header**. * **Operator**: Choose **Equals**. * **Value**: Enter the custom header and its value (e.g., `X-LoadForge-Test: true`). * **Then...**: * **Action**: Select **Allow**. 5. **Deploy the Rule**: * Save and deploy the rule to activate it immediately. ### Pros * **Secure Method**: Only requests with the specific header are allowed, minimizing security risks. * **No IP Management**: Eliminates the need to track and update IP ranges. ### Cons * **Complex Setup**: Requires precise configuration on both LoadForge and Cloudflare. * **Header Exposure Risk**: If the custom header becomes known, malicious actors could potentially exploit it. *** By implementing one of these methods, you can ensure that Cloudflare does not interfere with LoadForge's testing traffic, allowing for accurate and effective performance assessments of your website. # Firebase Authentication Source: https://docs.loadforge.com/examples/authentication-security/firebase-auth Integrate Firebase Authentication with LoadForge to simulate user authentication scenarios during load testing. ## Overview Integrating Firebase Authentication with Locust can enhance your load testing by simulating real-world user authentication scenarios. ## Setup **1. Set Up Firebase Authentication:** * **Create a Firebase Project:** If you haven't already, create a project in the [Firebase Console](https://console.firebase.google.com/). * **Enable Authentication Methods:** Navigate to the "Authentication" section and enable the desired sign-in methods, such as Email/Password, Google, or others. **2. Obtain Firebase Authentication Tokens:** To authenticate users during load testing, you'll need to generate Firebase Authentication tokens. This can be achieved using Firebase's REST API: * **Email/Password Authentication:** Send a POST request to Firebase's identity toolkit endpoint with the user's email and password to receive an ID token. Detailed instructions are available in the [Firebase Authentication REST API guide](https://firebase.google.com/docs/reference/rest/auth). * **Custom Authentication Systems:** If you're using a custom authentication system, you can integrate it with Firebase Authentication to generate tokens. This process is outlined in the [Firebase Authentication documentation](https://firebase.google.com/docs/auth). **3. Integrate Firebase Authentication with Locust:** Once you have the ID tokens, you can incorporate them into your Locust tests: * **Include the ID Token in Requests:** In your Locust tasks, add the `Authorization` header with the ID token for each request: ```python theme={null} from locust import HttpUser, task class FirebaseUser(HttpUser): def on_start(self): # Obtain the Firebase ID token self.id_token = "YOUR_FIREBASE_ID_TOKEN" @task def authenticated_request(self): headers = { "Authorization": f"Bearer {self.id_token}" } self.client.get("/your/protected/endpoint", headers=headers) ``` * **Automate Token Retrieval (Optional):** For dynamic token generation, implement a method to obtain the ID token programmatically within the `on_start` method. This ensures each simulated user has a unique token. # Getting Auth Tokens Source: https://docs.loadforge.com/examples/authentication-security/getting-auth-token Send a login request to get an authorization token for the rest of your test ## Overview Certain applications require a request in order to get an authentication token. This is common with APIs or SPAs. With LoadForge you can easily set any headers you need by tweaking your get() or post() calls to include a header set. Below is a partial test (just the `on_start` section) that shows how to set an authorization (or really any) header based on response content at launch with your load test. Naturally, you would not do all of these. They are 3 examples of getting a Bearer token in different ways. ## Code snippets These are just partial examples from a test. You may use the wizard or another example template and copy paste these into your test. ```python theme={null} from locust import HttpUser, TaskSet, task import requests class UserBehavior(TaskSet): def on_start(self): # Send login request response = requests.post("http://mysite.com/login", { "username": "user", "password": "pass" }) # THEN set "token" from response header self.client.headers.update({"Authorization": response.headers.get("token")}) # OR set "token" from response cookies self.client.cookies.set("Authorization", response.cookies.get("token")) # OR set "token" from response body token = response.json().get("token") self.client.headers.update({"Authorization": token}) # continue the rest of your test ``` *** LoadForge is powered by locust, meaning open source locust users can copy this script as well. If you are a locust user, consider importing your script to [LoadForge](https://loadforge.com) to supercharge your testing! # Simple JWT Authentication Testing Source: https://docs.loadforge.com/examples/authentication-security/jwt-token-refresh Basic JWT token authentication testing with login and protected API access This guide shows how to test APIs that use JWT (JSON Web Token) authentication. Perfect for testing login flows and authenticated API access. ## Use Cases * Test JWT-based login authentication * Test protected API endpoints with JWT tokens * Validate token-based access control * Test invalid token handling ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class JWTTestUser(HttpUser): def on_start(self): # User credentials for testing self.username = "test@example.com" self.password = "testpassword" self.access_token = None # API endpoints self.login_endpoint = "/auth/login" self.protected_endpoints = [ "/api/user/profile", "/api/user/data", "/api/dashboard", "/api/settings" ] # Login to get JWT token self.login() def login(self): """Login to get JWT access token""" login_data = { "username": self.username, "password": self.password } with self.client.post( self.login_endpoint, json=login_data, name="Auth: Login" ) as response: if response.status_code == 200: try: token_data = response.json() self.access_token = token_data.get('access_token') or token_data.get('token') print(f"Successfully logged in: {self.username}") except json.JSONDecodeError: print("Login successful but invalid JSON response") else: print(f"Login failed: {response.status_code}") response.failure(f"Login failed: {response.status_code}") def get_auth_headers(self): """Get authentication headers with JWT token""" if self.access_token: return { 'Authorization': f'Bearer {self.access_token}', 'Content-Type': 'application/json' } return {'Content-Type': 'application/json'} @task(4) def test_protected_api_access(self): """Test accessing protected API with JWT token""" if not self.access_token: self.login() endpoint = random.choice(self.protected_endpoints) headers = self.get_auth_headers() with self.client.get( endpoint, headers=headers, name="Protected API Access" ) as response: if response.status_code == 200: print(f"Protected API access success: {endpoint}") elif response.status_code == 401: print(f"JWT authentication failed: {endpoint}") response.failure("JWT token rejected") # Token might be expired, try to login again self.access_token = None else: response.failure(f"Unexpected response: {response.status_code}") @task(3) def test_user_profile_update(self): """Test updating user profile with JWT authentication""" if not self.access_token: self.login() headers = self.get_auth_headers() update_data = { "name": "Test User Updated", "email": self.username, "preferences": {"theme": "dark", "notifications": True} } with self.client.put( "/api/user/profile", json=update_data, headers=headers, name="Update Profile" ) as response: if response.status_code in [200, 204]: print("Profile update successful") elif response.status_code == 401: response.failure("JWT authentication failed on update") self.access_token = None else: response.failure(f"Profile update failed: {response.status_code}") @task(2) def test_invalid_token_access(self): """Test API access with invalid JWT token""" endpoint = random.choice(self.protected_endpoints) # Use invalid token invalid_headers = { 'Authorization': 'Bearer invalid-jwt-token-12345', 'Content-Type': 'application/json' } with self.client.get( endpoint, headers=invalid_headers, name="Invalid Token Access" ) as response: if response.status_code == 401: print(f"Invalid token correctly rejected: {endpoint}") elif response.status_code == 200: response.failure("Invalid JWT token was accepted") else: print(f"Invalid token returned {response.status_code}") @task(2) def test_missing_token_access(self): """Test API access without JWT token""" endpoint = random.choice(self.protected_endpoints) with self.client.get( endpoint, name="Missing Token Access" ) as response: if response.status_code == 401: print(f"Missing token correctly rejected: {endpoint}") elif response.status_code == 200: response.failure("Request without JWT token was accepted") else: print(f"Missing token returned {response.status_code}") @task(1) def test_login_with_invalid_credentials(self): """Test login with invalid credentials""" invalid_login_data = { "username": "invalid@example.com", "password": "wrongpassword" } with self.client.post( self.login_endpoint, json=invalid_login_data, name="Invalid Login" ) as response: if response.status_code == 401: print("Invalid credentials correctly rejected") elif response.status_code == 200: response.failure("Invalid credentials were accepted") else: print(f"Invalid login returned {response.status_code}") ``` ## Setup Instructions 1. Replace `self.username` and `self.password` with valid test credentials 2. Update `self.login_endpoint` with your actual login endpoint 3. Update `self.protected_endpoints` with your protected API endpoints 4. Adjust token field name if your API returns different field (token vs access\_token) ## What This Tests * **Login Flow**: Tests JWT token acquisition through login * **Protected APIs**: Tests access to JWT-protected endpoints * **Authentication**: Validates Bearer token authentication * **Error Handling**: Tests invalid/missing token scenarios ## Best Practices * Use dedicated test accounts with appropriate permissions * Handle token expiration by re-authenticating * Test both valid and invalid authentication scenarios * Monitor API response times with different token states ## Common Issues * **Token Expiration**: JWT tokens expire, implement re-authentication * **Token Format**: Ensure Bearer token format matches API expectations * **Field Names**: APIs may return 'token', 'access\_token', or 'jwt' * **HTTPS Only**: JWT tokens should only be sent over secure connections # Login Examples Source: https://docs.loadforge.com/examples/authentication-security/login-examples Test that logs in and then requests a profile page. ## Overview LoadForge allows you to login to any website, and then browse the site as a logged in user (or set of users). This example test posts to a login page when it starts, then requests /profile and some static content normally. This is a great example for logging into a site and then requesting content after login. The `on_start` definition will be run once by each individual simulated user. So they all login first, and then execute your various tests. LoadForge will automatically keep sessions, meaning you can login and then browse the site like a browser would. ## Basic code example ```python theme={null} import time from locust import HttpUser, task, between class QuickstartUser(HttpUser): def on_start(self): # Form post to /login with email and password self.client.post("/login", { 'email': 'user@domain.com', 'password': 'passw0rd' }) @task(2) def profiles(self): self.client.get("/profile") @task(1) def statics(self): self.client.get("/img/logo.png") self.client.get("/css/styles.css") self.client.get("/js/scripts.js") ``` ## Advanced code example This test starts with a login and a request to the index page of the site. It then finds all anchor tags (a href) and randomly browses through them. ```python theme={null} import random from locust import HttpUser, TaskSet, task from pyquery import PyQuery class AwesomeUser(HttpUser): def login(self): self.client.post("/login", { "username":"EXAMPLE_USER", "password":"PASSWORD" }) def index_page(self): r = self.client.get("/") pq = PyQuery(r.content) link_elements = pq("a") self.toc_urls = [] for l in link_elements: if "href" in l.attrib: self.toc_urls.append(l.attrib["href"]) def on_start(self): self.login() self.index_page() @task def load_page(self): url = random.choice(self.toc_urls) r = self.client.get(url) ``` ## Using basic authentication You can easily login using basic auth with any LoadForge request by adding the auth parameter to your get or post request object. This example demonstrates logging in as user1 with password password1. This guide includes two examples: a POST request with data and a GET request with authentication. ```python theme={null} import time from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(3, 5) @task def login_samples(self): self.client.get("/myUrl", auth=("user1", "password1")) self.client.post("/myUrl", data={"item": "123"}, auth=("user1", "password1")) ``` In addition, you can also use the on\_start definition to globally set them: ```python theme={null} from locust import HttpUser, task, between import base64 class WebsiteUser(HttpUser): wait_time = between(1, 2) def on_start(self): username = "user1" password = "password1" credentials = f"{username}:{password}" encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8") self.client.headers.update({ "Authorization": f"Basic {encoded_credentials}" }) @task def index(self): self.client.get("/") @task def dashboard(self): self.client.get("/dashboard") ``` ## CSRF tokens For login pages that require CSRF protection, see the guide on [Handling CSRF](/docs/tests-csrf). *** LoadForge is powered by locust, meaning open source locust users can copy this script as well. If you are a locust user, consider importing your script to [LoadForge](https://loadforge.com) to supercharge your testing! # Multi-user Login Examples Source: https://docs.loadforge.com/examples/authentication-security/multiple-users How to login as different users throughout the test, simulating multiple user logins and site usage. ## Overview LoadForge allows you to login to any website, and then browse the site as a logged in user (or set of users). We recommend learning about [regular user logins](login-examples) before this guide. LoadForge will automatically keep sessions, meaning you can login and then browse the site like a browser would. This guide will give you an example of how to login as different users throughout the test, simulating multiple user logins and site usage. Note that this typically isn't needed, as 1000 requests from a single login is generally the same load as 1000 separate users. ## Generated users code example ```python theme={null} from locust import HttpUser, TaskSet, task, between import time class AwesomeUser(HttpUser): wait_time = between(10, 20) # On start populate our user list def on_start(self): self.createUsernames() # Create an array of user1 -> user1000 def createUsernames(self): self.users = [] for i in range(1000): self.users.append("user"+str(i+1)) # Fetch the next user in our list def getUsername(self): return self.users.pop() # Login to the site def login(self): username = self.getUsername() loginInformation = { "username": username, "password":"passw0rd" } self.client.post("/login", loginInformation) # Logout from the site def logout(self): self.client.get('/logout', name="logout") # Define our tasks here @task(1) def load_page(self): # Login first self.login() # Put your logged in tasks here self.client.get('/profile/info') # Logout now self.logout() ``` The above code creates 1000 users named "user1" through "user1000" and logs in with each of those. It's designed for a 1000 user test of course. You can easily modify this. You can also pick usernames at random like so: ```python theme={null} import random def getUsername(self): return "user" + str(random.randint(1, 1000)) ``` You can modify this test to suit whatever pre-populated user system you have. You could also consider registering users with the test and clearing your database afterwards. *** LoadForge is powered by locust, meaning open source locust users can copy this script as well. If you are a locust user, consider importing your script to [LoadForge](https://loadforge.com) to supercharge your testing! # OAuth 2.0 Authorization Code Flow with PKCE Source: https://docs.loadforge.com/examples/authentication-security/oauth2-flow Complete OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange) for secure authentication testing This locustfile demonstrates how to test a complete OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange). This is commonly used by modern web applications and SPAs for secure authentication. ## Use Cases * Testing OAuth 2.0 providers (Auth0, Okta, Google, etc.) * Load testing authentication flows in SPAs * Validating OAuth security implementations * Testing token refresh mechanisms ## Complete Locustfile ```python theme={null} import base64 import hashlib import secrets import urllib.parse from locust import HttpUser, task, between import json import re class OAuth2User(HttpUser): wait_time = between(1, 3) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # OAuth 2.0 Configuration - Update these for your provider self.client_id = "your-client-id" self.redirect_uri = "https://your-app.com/callback" self.auth_server = "https://your-auth-server.com" self.scope = "openid profile email" # PKCE parameters self.code_verifier = None self.code_challenge = None self.state = None self.authorization_code = None self.access_token = None self.refresh_token = None def generate_pkce_parameters(self): """Generate PKCE code verifier and challenge""" # Generate code verifier (43-128 characters) self.code_verifier = base64.urlsafe_b64encode( secrets.token_bytes(32) ).decode('utf-8').rstrip('=') # Generate code challenge challenge_bytes = hashlib.sha256(self.code_verifier.encode('utf-8')).digest() self.code_challenge = base64.urlsafe_b64encode(challenge_bytes).decode('utf-8').rstrip('=') # Generate state parameter self.state = secrets.token_urlsafe(32) def on_start(self): """Initialize OAuth flow when user starts""" self.generate_pkce_parameters() self.initiate_oauth_flow() def initiate_oauth_flow(self): """Step 1: Initiate OAuth 2.0 authorization request""" auth_params = { 'response_type': 'code', 'client_id': self.client_id, 'redirect_uri': self.redirect_uri, 'scope': self.scope, 'state': self.state, 'code_challenge': self.code_challenge, 'code_challenge_method': 'S256' } auth_url = f"{self.auth_server}/oauth/authorize?" + urllib.parse.urlencode(auth_params) with self.client.get( auth_url, name="OAuth: Authorization Request", catch_response=True, allow_redirects=False ) as response: if response.status_code in [200, 302]: response.success() # In a real scenario, user would login here # For testing, we simulate getting the authorization code self.simulate_user_login() else: response.failure(f"Authorization request failed: {response.status_code}") def simulate_user_login(self): """Step 2: Simulate user login and consent""" # This simulates the user login form submission login_data = { 'username': 'test@example.com', 'password': 'testpassword', 'state': self.state } with self.client.post( f"{self.auth_server}/oauth/login", data=login_data, name="OAuth: User Login", catch_response=True, allow_redirects=False ) as response: if response.status_code in [200, 302]: response.success() # Extract authorization code from redirect if 'Location' in response.headers: location = response.headers['Location'] self.extract_authorization_code(location) else: # Sometimes the code is in the response body self.extract_authorization_code_from_body(response.text) else: response.failure(f"User login failed: {response.status_code}") def extract_authorization_code(self, redirect_url): """Extract authorization code from redirect URL""" parsed_url = urllib.parse.urlparse(redirect_url) query_params = urllib.parse.parse_qs(parsed_url.query) if 'code' in query_params: self.authorization_code = query_params['code'][0] # Verify state parameter if 'state' in query_params and query_params['state'][0] == self.state: self.exchange_code_for_tokens() else: print("State parameter mismatch - potential CSRF attack") else: print("No authorization code received") def extract_authorization_code_from_body(self, response_body): """Extract authorization code from response body (alternative method)""" # Look for authorization code in response body code_match = re.search(r'code=([^&\s]+)', response_body) if code_match: self.authorization_code = code_match.group(1) self.exchange_code_for_tokens() def exchange_code_for_tokens(self): """Step 3: Exchange authorization code for access token""" if not self.authorization_code: return token_data = { 'grant_type': 'authorization_code', 'client_id': self.client_id, 'code': self.authorization_code, 'redirect_uri': self.redirect_uri, 'code_verifier': self.code_verifier } with self.client.post( f"{self.auth_server}/oauth/token", data=token_data, name="OAuth: Token Exchange", catch_response=True ) as response: if response.status_code == 200: response.success() token_response = response.json() self.access_token = token_response.get('access_token') self.refresh_token = token_response.get('refresh_token') print(f"Successfully obtained access token") else: response.failure(f"Token exchange failed: {response.status_code}") @task(3) def make_authenticated_request(self): """Make authenticated API requests using the access token""" if not self.access_token: return headers = { 'Authorization': f'Bearer {self.access_token}', 'Content-Type': 'application/json' } with self.client.get( "/api/user/profile", headers=headers, name="API: Authenticated Request", catch_response=True ) as response: if response.status_code == 200: response.success() elif response.status_code == 401: # Token might be expired, try to refresh response.failure("Access token expired") self.refresh_access_token() else: response.failure(f"Authenticated request failed: {response.status_code}") @task(1) def refresh_access_token(self): """Step 4: Refresh access token using refresh token""" if not self.refresh_token: return refresh_data = { 'grant_type': 'refresh_token', 'client_id': self.client_id, 'refresh_token': self.refresh_token } with self.client.post( f"{self.auth_server}/oauth/token", data=refresh_data, name="OAuth: Token Refresh", catch_response=True ) as response: if response.status_code == 200: response.success() token_response = response.json() self.access_token = token_response.get('access_token') # Some providers issue new refresh tokens if 'refresh_token' in token_response: self.refresh_token = token_response['refresh_token'] print("Successfully refreshed access token") else: response.failure(f"Token refresh failed: {response.status_code}") @task(1) def revoke_token(self): """Optional: Revoke tokens (logout)""" if not self.access_token: return revoke_data = { 'token': self.access_token, 'client_id': self.client_id } with self.client.post( f"{self.auth_server}/oauth/revoke", data=revoke_data, name="OAuth: Token Revocation", catch_response=True ) as response: if response.status_code in [200, 204]: response.success() self.access_token = None self.refresh_token = None print("Successfully revoked tokens") else: response.failure(f"Token revocation failed: {response.status_code}") ``` ## Configuration Before running this test in LoadForge, update these variables: ```python theme={null} # OAuth 2.0 Provider Configuration self.client_id = "your-oauth-client-id" self.redirect_uri = "https://your-app.com/callback" self.auth_server = "https://your-auth-provider.com" self.scope = "openid profile email" # Adjust scopes as needed ``` ## LoadForge Setup 1. **Upload the locustfile** to LoadForge 2. **Configure your target host** to point to your application's API 3. **Set environment variables** for sensitive data like client secrets 4. **Choose appropriate user count** - OAuth flows can be resource-intensive 5. **Monitor token refresh patterns** in the results ## Key Testing Scenarios * **Authorization Flow Performance**: How quickly can users complete OAuth login? * **Token Exchange Latency**: Time taken to exchange codes for tokens * **Refresh Token Behavior**: How well does your app handle token refresh under load? * **Concurrent Authentication**: Multiple users authenticating simultaneously * **Token Expiration Handling**: Graceful handling of expired tokens ## Security Considerations * ✅ **PKCE Implementation**: Protects against authorization code interception * ✅ **State Parameter**: Prevents CSRF attacks * ✅ **Secure Token Storage**: Tokens are stored securely in memory * ✅ **Token Revocation**: Proper cleanup when sessions end ## Common OAuth Providers This pattern works with: * **Auth0**: `https://your-domain.auth0.com` * **Okta**: `https://your-domain.okta.com` * **Google**: `https://accounts.google.com` * **Microsoft**: `https://login.microsoftonline.com` * **GitHub**: `https://github.com/login/oauth` ## Troubleshooting * **Invalid Client**: Check your `client_id` configuration * **Redirect URI Mismatch**: Ensure redirect URI matches OAuth app settings * **PKCE Errors**: Verify code challenge generation is correct * **Scope Issues**: Check if requested scopes are allowed for your client When you add this test to LoadForge, you'll be able to see detailed metrics on each step of the OAuth flow, helping you identify bottlenecks in your authentication system. # Simple API Key Testing Source: https://docs.loadforge.com/examples/authentication-security/simple-api-key-testing Basic API key authentication testing with header and query methods This guide shows how to test APIs that use API key authentication. Perfect for testing different API key placement methods and validation. ## Use Cases * Test header-based API key authentication * Test query parameter API key authentication * Test invalid API key error handling * Validate missing API key responses ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random class APIKeyTestUser(HttpUser): def on_start(self): # API keys for testing (replace with your actual keys) self.valid_key = "sk-1234567890abcdef" self.invalid_key = "invalid-key-12345" # API endpoints that require authentication self.endpoints = ["/api/users", "/api/data", "/api/reports"] @task(4) def test_header_api_key(self): """Test API key in header (X-API-Key: your-key)""" endpoint = random.choice(self.endpoints) headers = { "X-API-Key": self.valid_key, "Content-Type": "application/json" } with self.client.get( endpoint, headers=headers, name="Header API Key" ) as response: if response.status_code == 200: print(f"Header API key success: {endpoint}") elif response.status_code == 401: response.failure("Valid API key rejected") else: response.failure(f"Unexpected response: {response.status_code}") @task(3) def test_bearer_token_api_key(self): """Test API key as Bearer token (Authorization: Bearer your-key)""" endpoint = random.choice(self.endpoints) headers = { "Authorization": f"Bearer {self.valid_key}", "Content-Type": "application/json" } with self.client.get( endpoint, headers=headers, name="Bearer Token API Key" ) as response: if response.status_code == 200: print(f"Bearer token success: {endpoint}") elif response.status_code == 401: response.failure("Valid bearer token rejected") else: response.failure(f"Unexpected response: {response.status_code}") @task(3) def test_query_parameter_api_key(self): """Test API key in query parameter (?api_key=your-key)""" endpoint = random.choice(self.endpoints) params = {"api_key": self.valid_key} with self.client.get( endpoint, params=params, name="Query Parameter API Key" ) as response: if response.status_code == 200: print(f"Query parameter success: {endpoint}") elif response.status_code == 401: response.failure("Valid API key in query rejected") else: response.failure(f"Unexpected response: {response.status_code}") @task(2) def test_invalid_api_key(self): """Test with invalid API key""" endpoint = random.choice(self.endpoints) headers = { "X-API-Key": self.invalid_key, "Content-Type": "application/json" } with self.client.get( endpoint, headers=headers, name="Invalid API Key" ) as response: if response.status_code == 401: print(f"Invalid API key correctly rejected: {endpoint}") elif response.status_code == 200: response.failure("Invalid API key was accepted") else: print(f"Invalid API key returned {response.status_code}") @task(1) def test_missing_api_key(self): """Test request without API key""" endpoint = random.choice(self.endpoints) with self.client.get( endpoint, name="Missing API Key" ) as response: if response.status_code == 401: print(f"Missing API key correctly rejected: {endpoint}") elif response.status_code == 200: response.failure("Request without API key was accepted") else: print(f"Missing API key returned {response.status_code}") ``` ## Setup Instructions 1. Replace `self.valid_key` with your actual API key 2. Replace `self.invalid_key` with a test invalid key 3. Update `self.endpoints` with your actual API endpoints 4. Adjust authentication method if your API uses different headers ## What This Tests * **Header Authentication**: Tests X-API-Key header method * **Bearer Token**: Tests Authorization: Bearer token method * **Query Parameter**: Tests api\_key query parameter method * **Error Handling**: Validates rejection of invalid/missing keys ## Best Practices * Use realistic API keys that match your API's format * Test all authentication methods your API supports * Validate both success and failure scenarios * Monitor response times for different auth methods ## Common Issues * **API Key Format**: Ensure test keys match your API's expected format * **Rate Limiting**: Some APIs rate limit by API key, adjust test frequency * **Permissions**: Different API keys may have different permissions * **Expiration**: Check if your test API keys expire # Basic Browser Navigation Source: https://docs.loadforge.com/examples/browser-testing/basic-browser-navigation Test multi-page navigation flows using Playwright with LoadForge This guide demonstrates how to test basic browser navigation flows using Playwright in LoadForge. Perfect for testing user journeys across multiple pages and measuring navigation performance. ## Use Cases * Testing multi-page user flows * Measuring page load performance across navigation * Validating navigation elements and links * Testing complex user journeys ## Key Features * **Multi-page Navigation**: Navigate through different pages and sections * **Performance Timing**: Measure individual page load times * **Element Interaction**: Click links and buttons to navigate * **Different Navigation Methods**: Text-based, CSS selectors, and XPath ## Example Implementation ```python theme={null} from locust import task from locust_plugins.users.playwright import PlaywrightUser, PageWithRetry, pw, event class NavigationUser(PlaywrightUser): @task(3) @pw async def home_to_products_flow(self, page: PageWithRetry): """Navigate from home to products section""" async with event(self, "Homepage Load"): await page.goto("/") async with event(self, "Products Page"): async with page.expect_navigation(): await page.click('a:has-text("Products")') async with event(self, "Product Detail"): async with page.expect_navigation(): await page.click('.product-item:first-child a') @task(2) @pw async def about_contact_flow(self, page: PageWithRetry): """Navigate through about and contact pages""" async with event(self, "About Page"): await page.goto("/about") async with event(self, "Contact Page"): async with page.expect_navigation(): await page.click('a[href="/contact"]') # Test different navigation methods async with event(self, "Services Page"): async with page.expect_navigation(): await page.click('nav ul li:nth-child(2) a') @task(1) @pw async def complex_navigation_flow(self, page: PageWithRetry): """Test complex multi-step navigation""" # Start from homepage async with event(self, "Homepage"): await page.goto("/") # Navigate to blog async with event(self, "Blog Section"): async with page.expect_navigation(): await page.click('//nav//a[contains(., "Blog")]') # Click on first blog post async with event(self, "Blog Post"): async with page.expect_navigation(): await page.click('.blog-post:first-child .read-more') # Navigate back using browser back async with event(self, "Back to Blog"): await page.go_back() # Navigate to homepage using logo async with event(self, "Back to Home"): async with page.expect_navigation(): await page.click('.logo') @task(1) @pw async def search_and_navigate(self, page: PageWithRetry): """Test search functionality and result navigation""" async with event(self, "Search Page"): await page.goto("/") # Perform search await page.fill('input[name="search"]', 'test query') async with event(self, "Search Results"): async with page.expect_navigation(): await page.click('button:has-text("Search")') # Click on first result if await page.locator('.search-result').count() > 0: async with event(self, "Search Result Detail"): async with page.expect_navigation(): await page.click('.search-result:first-child a') ``` ## Navigation Strategies ### 1. Text-Based Navigation ```python theme={null} await page.click('a:has-text("Products")') await page.click('button:has-text("Learn More")') ``` ### 2. CSS Selector Navigation ```python theme={null} await page.click('#main-nav a[href="/services"]') await page.click('.product-card:first-child .view-details') ``` ### 3. XPath Navigation ```python theme={null} await page.click('//nav//a[contains(., "Contact")]') await page.click('//footer//a[@href="/privacy"]') ``` ### 4. nth-child Navigation ```python theme={null} await page.click('nav ul li:nth-child(3) a') await page.click('.menu-items:nth-child(2)') ``` ## Performance Considerations * Use `event()` context manager to measure specific navigation steps * Combine `page.expect_navigation()` with clicks to ensure proper timing * Test navigation performance under different network conditions * Measure total user journey time vs individual page loads ## Common Patterns ### Breadcrumb Navigation Testing ```python theme={null} @task @pw async def breadcrumb_navigation(self, page: PageWithRetry): await page.goto("/products/category/item") # Navigate using breadcrumbs async with event(self, "Breadcrumb to Category"): async with page.expect_navigation(): await page.click('.breadcrumb a:has-text("Category")') async with event(self, "Breadcrumb to Products"): async with page.expect_navigation(): await page.click('.breadcrumb a:has-text("Products")') ``` ### Menu Navigation Testing ```python theme={null} @task @pw async def menu_navigation(self, page: PageWithRetry): await page.goto("/") # Test dropdown menu navigation await page.hover('.nav-dropdown') async with event(self, "Dropdown Item"): async with page.expect_navigation(): await page.click('.dropdown-menu a:first-child') ``` ## Testing Tips 1. **Wait for Navigation**: Always use `page.expect_navigation()` for reliable navigation timing 2. **Element Visibility**: Check if navigation elements are visible before clicking 3. **Multiple Strategies**: Test different ways users might navigate (links, buttons, menus) 4. **Back/Forward**: Include browser back/forward button testing in user flows 5. **Error Handling**: Test navigation to non-existent pages and error scenarios This script provides comprehensive browser navigation testing that measures performance while validating different navigation patterns your users might follow. # Form Testing Source: https://docs.loadforge.com/examples/browser-testing/form-testing Test form submission and validation using Playwright with LoadForge This guide demonstrates how to test form submission and validation using Playwright in LoadForge. Perfect for testing user registration, login forms, contact forms, and complex multi-step forms. ## Use Cases * Testing user registration and login flows * Validating form field validation rules * Testing multi-step form processes * Checking form error handling and success states ## Key Features * **Form Field Testing**: Fill various input types (text, email, password, select) * **Validation Testing**: Test client-side and server-side validation * **Error Handling**: Test form error states and messages * **Success Flow Testing**: Validate successful form submissions ## Example Implementation ```python theme={null} from locust import task from locust_plugins.users.playwright import PlaywrightUser, PageWithRetry, pw, event from faker import Faker fake = Faker() class FormTestingUser(PlaywrightUser): @task(3) @pw async def login_form_test(self, page: PageWithRetry): """Test login form submission and validation""" async with event(self, "Load Login Page"): await page.goto("/login") # Test valid login await page.fill('input[name="email"]', 'test@example.com') await page.fill('input[name="password"]', 'validpassword123') async with event(self, "Valid Login Submit"): async with page.expect_navigation(): await page.click('button:has-text("Log in")') # Verify successful login assert await page.locator('text=Dashboard').is_visible(), "Login failed" @task(2) @pw async def registration_form_test(self, page: PageWithRetry): """Test user registration form with validation""" async with event(self, "Load Registration Page"): await page.goto("/register") # Fill registration form with generated data await page.fill('input[name="first_name"]', fake.first_name()) await page.fill('input[name="last_name"]', fake.last_name()) await page.fill('input[name="email"]', fake.email()) await page.fill('input[name="password"]', 'SecurePass123!') await page.fill('input[name="password_confirmation"]', 'SecurePass123!') # Select from dropdown await page.select_option('select[name="country"]', 'US') # Check terms checkbox await page.check('input[name="terms"]') async with event(self, "Registration Submit"): async with page.expect_navigation(): await page.click('button:has-text("Register")') # Verify registration success success_msg = await page.locator('.success-message').is_visible() assert success_msg, "Registration did not show success message" @task(2) @pw async def contact_form_test(self, page: PageWithRetry): """Test contact form with file upload""" async with event(self, "Load Contact Page"): await page.goto("/contact") # Fill contact form await page.fill('input[name="name"]', fake.name()) await page.fill('input[name="email"]', fake.email()) await page.fill('input[name="subject"]', 'Test Subject') await page.fill('textarea[name="message"]', fake.text(max_nb_chars=200)) # Select inquiry type await page.select_option('select[name="inquiry_type"]', 'support') async with event(self, "Contact Form Submit"): await page.click('button:has-text("Send Message")') # Wait for success message without navigation await page.wait_for_selector('.alert-success', timeout=5000) @task(1) @pw async def form_validation_test(self, page: PageWithRetry): """Test form validation errors""" async with event(self, "Load Form for Validation"): await page.goto("/register") # Test email validation await page.fill('input[name="email"]', 'invalid-email') await page.fill('input[name="password"]', '123') # Too short async with event(self, "Submit Invalid Form"): await page.click('button:has-text("Register")') # Check for validation errors email_error = await page.locator('.error:has-text("email")').is_visible() password_error = await page.locator('.error:has-text("password")').is_visible() assert email_error or password_error, "Form validation errors not displayed" @task(1) @pw async def multi_step_form_test(self, page: PageWithRetry): """Test multi-step form process""" async with event(self, "Load Multi-step Form"): await page.goto("/onboarding") # Step 1: Personal Info await page.fill('input[name="first_name"]', fake.first_name()) await page.fill('input[name="last_name"]', fake.last_name()) await page.fill('input[name="phone"]', fake.phone_number()) async with event(self, "Step 1 Complete"): await page.click('button:has-text("Next")') # Step 2: Address Info await page.fill('input[name="address"]', fake.address()) await page.fill('input[name="city"]', fake.city()) await page.fill('input[name="zip"]', fake.zipcode()) await page.select_option('select[name="state"]', 'CA') async with event(self, "Step 2 Complete"): await page.click('button:has-text("Next")') # Step 3: Preferences await page.check('input[name="newsletter"]') await page.select_option('select[name="language"]', 'en') async with event(self, "Final Submit"): await page.click('button:has-text("Complete")') # Verify completion await page.wait_for_selector('.onboarding-complete') @task(1) @pw async def dynamic_form_test(self, page: PageWithRetry): """Test forms with dynamic fields""" async with event(self, "Load Dynamic Form"): await page.goto("/dynamic-form") # Select option that shows additional fields await page.select_option('select[name="user_type"]', 'business') # Wait for dynamic fields to appear await page.wait_for_selector('input[name="company_name"]') # Fill dynamic fields await page.fill('input[name="company_name"]', fake.company()) await page.fill('input[name="tax_id"]', '12-3456789') # Fill regular fields await page.fill('input[name="email"]', fake.email()) async with event(self, "Dynamic Form Submit"): await page.click('button:has-text("Submit")') # Verify submission await page.wait_for_selector('.form-success') ``` ## Form Field Strategies ### 1. Text Input Fields ```python theme={null} await page.fill('input[name="username"]', 'testuser') await page.fill('input[type="email"]', 'test@example.com') await page.fill('textarea[name="description"]', 'Long text content') ``` ### 2. Select Dropdowns ```python theme={null} # By value await page.select_option('select[name="country"]', 'US') # By label await page.select_option('select[name="country"]', label='United States') # Multiple selections await page.select_option('select[name="skills"]', ['python', 'javascript']) ``` ### 3. Checkboxes and Radio Buttons ```python theme={null} # Check/uncheck await page.check('input[name="terms"]') await page.uncheck('input[name="newsletter"]') # Radio buttons await page.click('input[name="plan"][value="premium"]') ``` ### 4. File Uploads ```python theme={null} # Single file await page.set_input_files('input[type="file"]', 'test-file.pdf') # Multiple files await page.set_input_files('input[type="file"]', ['file1.pdf', 'file2.jpg']) ``` ## Validation Testing Patterns ### Client-Side Validation ```python theme={null} @task @pw async def client_validation_test(self, page: PageWithRetry): await page.goto("/form") # Test required field validation await page.click('button[type="submit"]') # Check for HTML5 validation is_valid = await page.evaluate(""" document.querySelector('form').checkValidity() """) assert not is_valid, "Form should be invalid when required fields are empty" ``` ### Server-Side Validation ```python theme={null} @task @pw async def server_validation_test(self, page: PageWithRetry): await page.goto("/form") # Submit invalid data await page.fill('input[name="email"]', 'existing@example.com') # Duplicate email await page.click('button[type="submit"]') # Wait for server validation error await page.wait_for_selector('.field-error:has-text("already exists")') ``` ## Advanced Form Testing ### CSRF Token Handling ```python theme={null} @task @pw async def csrf_protected_form(self, page: PageWithRetry): await page.goto("/protected-form") # CSRF token is automatically included in form await page.fill('input[name="data"]', 'test data') await page.click('button[type="submit"]') # Verify successful submission await page.wait_for_selector('.success-message') ``` ### Form with AJAX Submission ```python theme={null} @task @pw async def ajax_form_test(self, page: PageWithRetry): await page.goto("/ajax-form") await page.fill('input[name="email"]', fake.email()) # Click submit but don't expect navigation await page.click('button[type="submit"]') # Wait for AJAX response await page.wait_for_selector('.response-message') # Check if form was reset email_value = await page.input_value('input[name="email"]') assert email_value == '', "Form should be reset after successful AJAX submission" ``` ## Error Handling Patterns ### Network Error Testing ```python theme={null} @task @pw async def network_error_test(self, page: PageWithRetry): await page.goto("/form") # Simulate network failure await page.route('**/api/submit', lambda route: route.abort()) await page.fill('input[name="data"]', 'test') await page.click('button[type="submit"]') # Check error handling await page.wait_for_selector('.network-error') ``` ## Performance Considerations * Use `event()` to measure form submission times * Test forms under different network conditions * Validate that large forms don't cause performance issues * Test file upload performance with various file sizes ## Testing Tips 1. **Use Faker**: Generate realistic test data with the Faker library 2. **Test Edge Cases**: Empty forms, invalid data, boundary values 3. **Accessibility**: Test form navigation with keyboard-only interaction 4. **Mobile Forms**: Test form usability on different screen sizes 5. **Validation Messages**: Verify error messages are clear and helpful 6. **Form State**: Test form behavior when navigating away and returning This comprehensive form testing script covers various form scenarios and validation patterns commonly found in web applications. # Simple Mobile Browser Testing Source: https://docs.loadforge.com/examples/browser-testing/simple-mobile-testing Basic mobile browser testing with device emulation and touch interactions using Playwright This guide shows how to test mobile browser experiences with device emulation. Perfect for testing responsive designs and mobile-specific functionality. ## Use Cases * Test mobile responsive layouts * Validate touch interactions * Check mobile performance * Test different device sizes ## Simple Implementation ```python theme={null} from locust import task, HttpUser from locust_plugins.users.playwright import PlaywrightUser, pw class MobileBrowserUser(PlaywrightUser): def on_start(self): # Mobile device configurations self.devices = [ {"name": "iPhone 12", "width": 390, "height": 844}, {"name": "Samsung Galaxy", "width": 360, "height": 640}, {"name": "iPad", "width": 768, "height": 1024} ] # Test URLs self.test_urls = [ "/", "/products", "/about", "/contact", "/login" ] @task(3) @pw def test_mobile_navigation(self, page): """Test mobile navigation and menu""" device = self.devices[0] # iPhone 12 # Set mobile viewport page.set_viewport_size(width=device["width"], height=device["height"]) # Navigate to homepage page.goto("/") # Look for mobile menu button (hamburger menu) menu_selectors = [ "[data-testid='mobile-menu']", ".mobile-menu-button", ".hamburger", "button[aria-label*='menu']" ] menu_button = None for selector in menu_selectors: if page.locator(selector).count() > 0: menu_button = page.locator(selector) break if menu_button: # Tap mobile menu menu_button.tap() # Wait for menu to open page.wait_for_timeout(500) # Check if navigation links are visible nav_links = page.locator("nav a, .menu a").count() print(f"Mobile menu opened with {nav_links} links") else: print("No mobile menu found - testing regular navigation") # Test scrolling page.evaluate("window.scrollTo(0, 500)") page.wait_for_timeout(300) @task(2) @pw def test_responsive_layout(self, page): """Test responsive layout at different screen sizes""" url = self.random_url() for device in self.devices: # Set viewport for each device page.set_viewport_size(width=device["width"], height=device["height"]) # Navigate to page page.goto(url) # Check if page loads properly page.wait_for_load_state("networkidle") # Take basic measurements body_width = page.evaluate("document.body.scrollWidth") viewport_width = device["width"] # Check for horizontal scrolling (usually bad on mobile) if body_width > viewport_width + 10: # Small tolerance print(f"Warning: Horizontal scroll on {device['name']} - {body_width}px > {viewport_width}px") else: print(f"{device['name']}: Layout fits properly") @task(2) @pw def test_touch_interactions(self, page): """Test touch-specific interactions""" device = self.devices[0] # Use iPhone for touch testing page.set_viewport_size(width=device["width"], height=device["height"]) page.goto("/") # Test tap interactions on buttons buttons = page.locator("button, .btn, [role='button']") button_count = buttons.count() if button_count > 0: # Tap first button first_button = buttons.first if first_button.is_visible(): first_button.tap() page.wait_for_timeout(300) print("Touch interaction: Button tap successful") # Test swipe gesture (if applicable) if page.locator(".carousel, .slider").count() > 0: carousel = page.locator(".carousel, .slider").first # Get carousel position box = carousel.bounding_box() if box: # Simulate swipe left start_x = box["x"] + box["width"] * 0.8 end_x = box["x"] + box["width"] * 0.2 y = box["y"] + box["height"] / 2 page.mouse.move(start_x, y) page.mouse.down() page.mouse.move(end_x, y) page.mouse.up() print("Touch interaction: Swipe gesture performed") @task(1) @pw def test_mobile_forms(self, page): """Test form interactions on mobile""" device = self.devices[0] page.set_viewport_size(width=device["width"], height=device["height"]) # Look for forms on common pages form_pages = ["/contact", "/login", "/signup", "/search"] for form_page in form_pages: try: page.goto(form_page) # Look for input fields inputs = page.locator("input[type='text'], input[type='email'], textarea") if inputs.count() > 0: first_input = inputs.first # Tap to focus (important on mobile) first_input.tap() # Type some text first_input.fill("Test input on mobile") # Check if virtual keyboard doesn't break layout page.wait_for_timeout(500) print(f"Mobile form test completed on {form_page}") break except Exception as e: # Page might not exist, continue to next continue def random_url(self): """Get a random test URL""" import random return random.choice(self.test_urls) ``` ## Setup Instructions 1. Ensure Playwright is configured in LoadForge 2. Test with real mobile device dimensions 3. Focus on common mobile interaction patterns 4. Check responsive breakpoints ## What This Tests * **Mobile Layout**: Responsive design at different screen sizes * **Touch Interactions**: Tap, swipe, and touch-specific gestures * **Mobile Navigation**: Hamburger menus and mobile-specific UI * **Form Usability**: Input field behavior on mobile devices ## Mobile Testing Tips * **Viewport Sizes**: Test common device resolutions * **Touch Targets**: Ensure buttons are large enough (44px minimum) * **Text Size**: Check readability on small screens * **Loading Speed**: Mobile users expect fast loading ## Common Mobile Issues * **Horizontal Scrolling**: Content wider than screen * **Small Touch Targets**: Buttons too small to tap easily * **Slow Loading**: Large images or resources * **Broken Navigation**: Mobile menus not working ## Device Dimensions Common mobile device sizes to test: * **iPhone SE**: 375x667 * **iPhone 12**: 390x844 * **Samsung Galaxy**: 360x640 * **iPad**: 768x1024 # Simple Performance Testing Source: https://docs.loadforge.com/examples/browser-testing/simple-performance-testing Basic browser performance testing with Core Web Vitals and loading metrics using Playwright This guide shows how to measure basic browser performance metrics including Core Web Vitals. Perfect for monitoring page loading speed and user experience. ## Use Cases * Measure Core Web Vitals (LCP, FCP, CLS) * Test page loading performance * Monitor resource loading times * Check performance across different pages ## Simple Implementation ```python theme={null} from locust import task, HttpUser from locust_plugins.users.playwright import PlaywrightUser, pw import time class PerformanceUser(PlaywrightUser): def on_start(self): # Pages to test performance self.test_pages = [ "/", "/products", "/about", "/blog", "/contact" ] @task(3) @pw def measure_core_web_vitals(self, page): """Measure Core Web Vitals for a page""" test_url = self.random_page() # Start timing start_time = time.time() # Navigate to page page.goto(test_url) # Wait for page to fully load page.wait_for_load_state("networkidle") # Measure First Contentful Paint (FCP) fcp = page.evaluate(""" () => { const entries = performance.getEntriesByType('paint'); const fcpEntry = entries.find(entry => entry.name === 'first-contentful-paint'); return fcpEntry ? fcpEntry.startTime : null; } """) # Measure Largest Contentful Paint (LCP) lcp = page.evaluate(""" () => { return new Promise((resolve) => { const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; resolve(lastEntry.startTime); }); observer.observe({entryTypes: ['largest-contentful-paint']}); // Fallback timeout setTimeout(() => resolve(null), 3000); }); } """) # Measure Cumulative Layout Shift (CLS) cls = page.evaluate(""" () => { let clsValue = 0; const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } }); observer.observe({entryTypes: ['layout-shift']}); return new Promise((resolve) => { setTimeout(() => resolve(clsValue), 2000); }); } """) # Calculate total load time load_time = (time.time() - start_time) * 1000 # Log results print(f"Performance for {test_url}:") print(f" Load Time: {load_time:.0f}ms") if fcp: print(f" FCP: {fcp:.0f}ms") if lcp: print(f" LCP: {lcp:.0f}ms") if cls: print(f" CLS: {cls:.3f}") @task(2) @pw def measure_resource_loading(self, page): """Measure resource loading performance""" test_url = self.random_page() # Track network requests requests = [] def handle_request(request): requests.append({ 'url': request.url, 'method': request.method, 'start_time': time.time() }) def handle_response(response): # Find matching request for req in requests: if req['url'] == response.url: req['response_time'] = time.time() - req['start_time'] req['status'] = response.status req['size'] = len(response.body()) if response.body() else 0 break page.on('request', handle_request) page.on('response', handle_response) # Navigate and measure page.goto(test_url) page.wait_for_load_state("networkidle") # Analyze resource performance total_size = sum(req.get('size', 0) for req in requests) slow_resources = [req for req in requests if req.get('response_time', 0) > 2.0] print(f"Resource loading for {test_url}:") print(f" Total requests: {len(requests)}") print(f" Total size: {total_size / 1024:.1f}KB") print(f" Slow resources (>2s): {len(slow_resources)}") @task(2) @pw def test_page_speed(self, page): """Test basic page loading speed""" test_url = self.random_page() # Measure different loading stages start_time = time.time() # Navigate to page page.goto(test_url) # Measure time to DOM content loaded dom_time = time.time() # Wait for all resources page.wait_for_load_state("networkidle") # Measure total load time total_time = time.time() # Calculate metrics dom_load_time = (dom_time - start_time) * 1000 total_load_time = (total_time - start_time) * 1000 # Get page size page_size = page.evaluate("document.documentElement.outerHTML.length") print(f"Page speed for {test_url}:") print(f" DOM Load: {dom_load_time:.0f}ms") print(f" Total Load: {total_load_time:.0f}ms") print(f" Page Size: {page_size / 1024:.1f}KB") # Performance thresholds if total_load_time > 3000: print(f" WARNING: Slow loading page ({total_load_time:.0f}ms)") @task(1) @pw def test_mobile_performance(self, page): """Test performance on mobile viewport""" test_url = self.random_page() # Set mobile viewport page.set_viewport_size(width=375, height=667) # Simulate slower mobile connection page.route("**/*", lambda route: ( time.sleep(0.1), # Add 100ms delay route.continue_() )) start_time = time.time() # Navigate and measure page.goto(test_url) page.wait_for_load_state("networkidle") load_time = (time.time() - start_time) * 1000 print(f"Mobile performance for {test_url}:") print(f" Mobile Load Time: {load_time:.0f}ms") # Mobile performance is typically slower if load_time > 5000: print(f" WARNING: Very slow on mobile ({load_time:.0f}ms)") def random_page(self): """Get a random test page""" import random return random.choice(self.test_pages) ``` ## Setup Instructions 1. Ensure Playwright is configured in LoadForge 2. Test with realistic network conditions 3. Focus on user-facing performance metrics 4. Set performance budgets for your site ## What This Tests * **Core Web Vitals**: LCP, FCP, and CLS measurements * **Loading Speed**: DOM and total page load times * **Resource Performance**: Network request timing and sizes * **Mobile Performance**: Performance on mobile devices ## Performance Metrics * **FCP (First Contentful Paint)**: When first content appears (\< 1.8s good) * **LCP (Largest Contentful Paint)**: When main content loads (\< 2.5s good) * **CLS (Cumulative Layout Shift)**: Visual stability (\< 0.1 good) * **Total Load Time**: Complete page loading (\< 3s good) ## Performance Tips * **Optimize Images**: Compress and use modern formats * **Minimize JavaScript**: Reduce bundle sizes * **Use CDN**: Serve static assets from edge locations * **Enable Caching**: Set proper cache headers ## Common Performance Issues * **Large Images**: Unoptimized images slow loading * **Too Much JavaScript**: Heavy JS bundles block rendering * **Slow Server**: Backend response times affect performance * **Layout Shifts**: Content moving during load hurts CLS # Data Retention Testing Source: https://docs.loadforge.com/examples/compliance-security/data-retention-testing Test your application's data retention policies and automated data lifecycle management for compliance requirements # Data Retention Testing Test your application's data retention policies and automated data lifecycle management. This locustfile validates retention periods, data archival, automated deletion, and compliance with regulatory requirements. ## Use Cases * **Retention Policy Validation**: Test automated data retention and deletion * **Data Archival**: Validate data archiving processes and accessibility * **Compliance Verification**: Ensure adherence to regulatory retention requirements * **Lifecycle Management**: Test data lifecycle from creation to deletion * **Storage Optimization**: Validate data purging and storage management ## Locustfile ```python theme={null} from locust import HttpUser, task, between import random import json import uuid from datetime import datetime, timedelta class DataRetentionUser(HttpUser): """Test data retention policies and lifecycle management""" wait_time = between(1, 3) def on_start(self): """Initialize data retention testing""" self.user_id = str(uuid.uuid4()) # Data retention policies (in days) self.retention_policies = { 'user_data': 2555, # 7 years 'transaction_data': 2555, # 7 years (financial) 'analytics_data': 730, # 2 years 'session_data': 30, # 30 days 'log_data': 90, # 90 days 'temporary_data': 1, # 1 day 'marketing_data': 365 # 1 year } print(f"Data retention testing initialized for user: {self.user_id}") @task(3) def test_data_creation_with_retention(self): """Test data creation with proper retention metadata""" data_type = random.choice(list(self.retention_policies.keys())) retention_days = self.retention_policies[data_type] # Calculate retention expiry date expiry_date = (datetime.utcnow() + timedelta(days=retention_days)).isoformat() test_data = { 'user_id': self.user_id, 'data_type': data_type, 'content': f'Test data for {data_type}', 'created_at': datetime.utcnow().isoformat(), 'retention_policy': data_type, 'retention_expiry': expiry_date } with self.client.post( '/api/v1/data/create', json=test_data, name=f"Create Data - {data_type}", catch_response=True ) as response: if response.status_code in [200, 201]: try: result = response.json() if 'data_id' in result and 'retention_expiry' in result: data_id = result['data_id'] print(f"Data created with retention: {data_id}") response.success() else: response.failure("Missing retention metadata in response") except json.JSONDecodeError: response.failure("Invalid data creation response") else: response.failure(f"Data creation failed: {response.status_code}") @task(2) def test_retention_policy_query(self): """Test querying retention policies""" with self.client.get( '/api/v1/data/retention-policies', name="Retention Policies Query", catch_response=True ) as response: if response.status_code == 200: try: policies = response.json() print(f"Retention policies validated: {len(policies.get('policies', []))} policies") response.success() except json.JSONDecodeError: response.failure("Invalid retention policies response") else: response.failure(f"Retention policies query failed: {response.status_code}") @task(2) def test_data_archival_process(self): """Test data archival for long-term retention""" archival_request = { 'user_id': self.user_id, 'data_category': 'user_data', 'archive_reason': 'compliance_requirement', 'requested_by': 'system_test' } with self.client.post( '/api/v1/data/archive', json=archival_request, name="Data Archival Request", catch_response=True ) as response: if response.status_code in [200, 202]: try: result = response.json() if 'archive_id' in result: archive_id = result['archive_id'] print(f"Data archival initiated: {archive_id}") response.success() else: response.failure("Missing archive ID in response") except json.JSONDecodeError: response.failure("Invalid archival response") else: response.failure(f"Data archival failed: {response.status_code}") @task(1) def test_automated_deletion_process(self): """Test automated data deletion for expired data""" deletion_request = { 'data_type': random.choice(list(self.retention_policies.keys())), 'deletion_reason': 'retention_expired', 'dry_run': True # Test mode } with self.client.post( '/api/v1/data/automated-deletion', json=deletion_request, name="Automated Deletion Test", catch_response=True ) as response: if response.status_code == 200: try: result = response.json() if 'deletion_summary' in result: summary = result['deletion_summary'] records_to_delete = summary.get('records_to_delete', 0) print(f"Automated deletion summary: {records_to_delete} records") response.success() else: response.failure("Missing deletion summary") except json.JSONDecodeError: response.failure("Invalid deletion response") else: response.failure(f"Automated deletion test failed: {response.status_code}") ``` ## Configuration Update these settings for your data retention testing: ```python theme={null} # Data Retention Policies (days) RETENTION_POLICIES = { 'user_data': 2555, # 7 years 'financial_data': 2555, # 7 years 'analytics_data': 730, # 2 years 'session_data': 30, # 30 days 'log_data': 90, # 90 days 'marketing_data': 365 # 1 year } ``` ## LoadForge Data Retention Testing ### Multi-Region Compliance Use LoadForge's region selection to test data retention across different jurisdictions: * **EU Regions**: Test GDPR compliance requirements * **US Regions**: Test SOX and other US regulatory requirements * **Global Testing**: Ensure consistent retention policies worldwide ### Key Validation Points * **Retention Policies**: Automated enforcement of retention periods * **Data Archival**: Proper archiving for long-term storage * **Automated Deletion**: Timely deletion of expired data * **Compliance Reporting**: Accurate retention compliance reports ## Data Retention Checklist * ✅ **Policy Definition**: Clear retention periods for each data type * ✅ **Automated Enforcement**: System-enforced retention rules * ✅ **Archival Process**: Proper data archiving procedures * ✅ **Deletion Automation**: Automated deletion of expired data * ✅ **Compliance Reporting**: Regular retention compliance reports ## Best Practices * **Clear Policies**: Define specific retention periods for each data type * **Automated Processes**: Implement automated retention enforcement * **Regular Audits**: Conduct periodic retention compliance reviews * **Secure Deletion**: Ensure secure deletion of sensitive data ## Troubleshooting * **Retention Violations**: Check automated policy enforcement * **Archival Failures**: Verify storage systems and processes * **Deletion Issues**: Review automated deletion procedures * **Compliance Gaps**: Audit retention policy coverage This locustfile ensures your application properly manages data retention and complies with regulatory requirements for data lifecycle management. # Security Headers Validation Source: https://docs.loadforge.com/examples/compliance-security/security-headers-validation Crawl your entire website and validate security headers implementation across all pages This guide demonstrates how to crawl your entire website and validate security headers implementation across all discovered pages, ensuring consistent security controls throughout your site. ## Use Cases * Validating security headers across all website pages * Ensuring consistent security implementation after deployments * Detecting missing security headers on specific page types * Monitoring security header configuration changes * Compliance checking for security standards ## Simple Security Headers Crawler ```python theme={null} from locust import HttpUser, task, between import re import time from urllib.parse import urlparse from collections import deque # CONFIGURATION - Edit these settings for your requirements REQUIRED_HEADERS = { 'Strict-Transport-Security': True, # HSTS - highly recommended 'X-Frame-Options': True, # Clickjacking protection 'X-Content-Type-Options': True, # MIME type sniffing protection 'Referrer-Policy': True, # Referrer information control 'Content-Security-Policy': False, # CSP - optional by default (can be complex) 'X-XSS-Protection': False, # Deprecated but still useful } # Optional: Validate specific header values HEADER_VALUES = { 'X-Content-Type-Options': ['nosniff'], 'X-Frame-Options': ['DENY', 'SAMEORIGIN'], 'Referrer-Policy': ['strict-origin-when-cross-origin', 'same-origin', 'no-referrer', 'strict-origin'] } class SecurityHeadersCrawler(HttpUser): wait_time = between(1, 2) def on_start(self): """Initialize security headers crawling""" self.visited_pages = set() self.pages_to_check = deque(['/']) self.security_issues = [] self.pages_checked = 0 self.base_domain = None print("Starting security headers validation...") print(f"Required headers: {[h for h, required in REQUIRED_HEADERS.items() if required]}") @task(5) def crawl_and_validate_headers(self): """Main crawling task to validate security headers""" if not self.pages_to_check: return current_page = self.pages_to_check.popleft() if current_page in self.visited_pages: return self.visited_pages.add(current_page) self.pages_checked += 1 with self.client.get(current_page, name=f"SECURITY: {current_page}", catch_response=True) as response: if response.status_code == 200: # Set base domain on first successful request if not self.base_domain: self.base_domain = urlparse(self.client.base_url).netloc # Validate security headers missing_headers, invalid_headers = self._validate_security_headers(current_page, response.headers) # Find more pages to crawl self._find_internal_pages(response.text) # Report results if missing_headers or invalid_headers: issues = missing_headers + invalid_headers failure_msg = f"❌ Security issues: {', '.join(issues)}" response.failure(failure_msg) else: response.success() else: response.failure(f"Could not access page: HTTP {response.status_code}") def _validate_security_headers(self, page_url, headers): """Validate security headers for a page""" missing_headers = [] invalid_headers = [] for header_name, is_required in REQUIRED_HEADERS.items(): if is_required: if header_name not in headers: missing_headers.append(f"Missing {header_name}") self._log_security_issue(page_url, 'HIGH', f'Missing required header: {header_name}') else: # Check header value if validation rules exist if header_name in HEADER_VALUES: header_value = headers[header_name] valid_values = HEADER_VALUES[header_name] # For some headers, check if value contains any of the valid options if header_name == 'Referrer-Policy': if not any(valid_val in header_value for valid_val in valid_values): invalid_headers.append(f"Invalid {header_name}") self._log_security_issue(page_url, 'MEDIUM', f'Invalid {header_name}: {header_value}') else: if header_value not in valid_values: invalid_headers.append(f"Invalid {header_name}") self._log_security_issue(page_url, 'MEDIUM', f'Invalid {header_name}: {header_value}') elif header_name in headers: # Optional header is present - validate it if header_name in HEADER_VALUES: header_value = headers[header_name] valid_values = HEADER_VALUES[header_name] if header_name == 'Referrer-Policy': if not any(valid_val in header_value for valid_val in valid_values): invalid_headers.append(f"Invalid {header_name}") self._log_security_issue(page_url, 'MEDIUM', f'Invalid optional {header_name}: {header_value}') else: if header_value not in valid_values: invalid_headers.append(f"Invalid {header_name}") self._log_security_issue(page_url, 'MEDIUM', f'Invalid optional {header_name}: {header_value}') return missing_headers, invalid_headers def _find_internal_pages(self, html_content): """Find internal pages from current page links""" if len(self.pages_to_check) > 50: # Limit crawling depth return # Find internal links links = re.findall(r']+href=["\']([^"\']+)["\']', html_content, re.IGNORECASE) for link in links: if self._is_internal_page_link(link): normalized_link = self._normalize_link(link) if normalized_link and normalized_link not in self.visited_pages: if normalized_link not in self.pages_to_check: self.pages_to_check.append(normalized_link) def _is_internal_page_link(self, link): """Check if link is an internal page (not resource)""" # Skip anchors, external protocols, and resources if any(skip in link.lower() for skip in ['#', 'mailto:', 'tel:', 'javascript:']): return False # Skip common resource extensions resource_extensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.pdf', '.zip', '.svg', '.ico', '.mp4', '.mp3', '.woff', '.woff2', '.ttf', '.eot'] if any(link.lower().endswith(ext) for ext in resource_extensions): return False # Must be internal (relative or same domain) if link.startswith('/') or not link.startswith('http'): return True if link.startswith('http') and self.base_domain: return urlparse(link).netloc == self.base_domain return False def _normalize_link(self, link): """Normalize link for checking""" try: if link.startswith('/'): return link.split('#')[0] # Remove fragment elif not link.startswith('http'): return '/' + link.lstrip('./') elif self.base_domain and link.startswith('http'): parsed = urlparse(link) if parsed.netloc == self.base_domain: return parsed.path return None except: return None def _log_security_issue(self, page_url, severity, description): """Log security header issue""" issue = { 'page': page_url, 'severity': severity, 'description': description, 'timestamp': time.time() } self.security_issues.append(issue) print(f"SECURITY ISSUE [{severity}]: {description} on {page_url}") @task(1) def report_security_status(self): """Report current security validation status""" if self.pages_checked < 3: return high_issues = [issue for issue in self.security_issues if issue['severity'] == 'HIGH'] medium_issues = [issue for issue in self.security_issues if issue['severity'] == 'MEDIUM'] print(f"SECURITY HEADERS STATUS: {len(self.security_issues)} total issues " f"({len(high_issues)} high, {len(medium_issues)} medium) " f"across {self.pages_checked} pages") def on_stop(self): """Final security headers report""" print("\n" + "="*50) print("SECURITY HEADERS VALIDATION COMPLETE") print("="*50) print(f"Pages checked: {self.pages_checked}") print(f"Total security issues: {len(self.security_issues)}") if self.security_issues: print(f"\nTOP SECURITY ISSUES:") for issue in self.security_issues[:10]: print(f"❌ [{issue['severity']}] {issue['description']}") print(f" Page: {issue['page']}") else: print("✅ All pages have proper security headers!") ``` ## Comprehensive Security Headers Validation ```python theme={null} from locust import HttpUser, task, between import re import time from urllib.parse import urlparse from collections import deque, defaultdict # COMPREHENSIVE CONFIGURATION SECURITY_HEADERS_CONFIG = { 'required_headers': { 'Strict-Transport-Security': { 'required': True, 'min_max_age': 31536000, # 1 year minimum 'should_include_subdomains': True }, 'X-Frame-Options': { 'required': True, 'valid_values': ['DENY', 'SAMEORIGIN'] }, 'X-Content-Type-Options': { 'required': True, 'valid_values': ['nosniff'] }, 'Referrer-Policy': { 'required': True, 'valid_values': ['strict-origin-when-cross-origin', 'same-origin', 'no-referrer', 'strict-origin'] }, 'Content-Security-Policy': { 'required': False, # Optional but recommended 'check_unsafe_directives': True }, 'Permissions-Policy': { 'required': False, # Modern replacement for Feature-Policy } }, 'page_type_requirements': { 'login_pages': ['X-Frame-Options', 'Strict-Transport-Security'], 'api_endpoints': ['X-Content-Type-Options', 'Strict-Transport-Security'], 'admin_pages': ['X-Frame-Options', 'Strict-Transport-Security', 'Content-Security-Policy'] } } class ComprehensiveSecurityValidator(HttpUser): wait_time = between(1, 3) def on_start(self): """Initialize comprehensive security validation""" self.visited_pages = set() self.pages_to_check = deque(['/']) self.security_issues = [] self.page_classifications = defaultdict(list) self.header_stats = defaultdict(int) self.base_domain = None print("Starting comprehensive security headers validation...") @task(5) def crawl_and_validate_comprehensive(self): """Comprehensive security headers validation""" if not self.pages_to_check: return current_page = self.pages_to_check.popleft() if current_page in self.visited_pages: return self.visited_pages.add(current_page) with self.client.get(current_page, name=f"SECURITY: {current_page}", catch_response=True) as response: if response.status_code == 200: if not self.base_domain: self.base_domain = urlparse(self.client.base_url).netloc # Classify page type page_type = self._classify_page_type(current_page, response.text) # Validate headers based on page type and general requirements issues = self._comprehensive_header_validation(current_page, response.headers, page_type) # Find more pages self._find_internal_pages(response.text) # Report results if issues: failure_msg = f"❌ {len(issues)} security issues found" response.failure(failure_msg) else: response.success() else: response.failure(f"Could not access page: HTTP {response.status_code}") def _classify_page_type(self, page_url, html_content): """Classify page type for specific security requirements""" page_type = 'general' # Check for login/auth pages if any(keyword in page_url.lower() for keyword in ['/login', '/signin', '/auth', '/register']): page_type = 'login_pages' elif any(keyword in html_content.lower() for keyword in [' 100: # Limit crawling depth return links = re.findall(r']+href=["\']([^"\']+)["\']', html_content, re.IGNORECASE) for link in links: if self._is_internal_page_link(link): normalized_link = self._normalize_link(link) if normalized_link and normalized_link not in self.visited_pages: if normalized_link not in self.pages_to_check: self.pages_to_check.append(normalized_link) def _is_internal_page_link(self, link): """Check if link is an internal page""" if any(skip in link.lower() for skip in ['#', 'mailto:', 'tel:', 'javascript:']): return False resource_extensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.pdf', '.zip', '.svg', '.ico', '.mp4', '.mp3', '.woff', '.woff2', '.ttf', '.eot'] if any(link.lower().endswith(ext) for ext in resource_extensions): return False if link.startswith('/') or not link.startswith('http'): return True if link.startswith('http') and self.base_domain: return urlparse(link).netloc == self.base_domain return False def _normalize_link(self, link): """Normalize link for checking""" try: if link.startswith('/'): return link.split('#')[0] elif not link.startswith('http'): return '/' + link.lstrip('./') elif self.base_domain and link.startswith('http'): parsed = urlparse(link) if parsed.netloc == self.base_domain: return parsed.path return None except: return None def _log_security_issue(self, page_url, severity, description): """Log security issue with details""" issue = { 'page': page_url, 'severity': severity, 'description': description, 'timestamp': time.time() } self.security_issues.append(issue) print(f"SECURITY ISSUE [{severity}]: {description} on {page_url}") @task(1) def generate_security_report(self): """Generate comprehensive security report""" if len(self.visited_pages) < 5: return high_issues = [i for i in self.security_issues if i['severity'] == 'HIGH'] medium_issues = [i for i in self.security_issues if i['severity'] == 'MEDIUM'] print(f"SECURITY REPORT: {len(self.security_issues)} total issues " f"({len(high_issues)} high, {len(medium_issues)} medium) " f"across {len(self.visited_pages)} pages") # Report page type distribution for page_type, pages in self.page_classifications.items(): print(f" {page_type}: {len(pages)} pages") def on_stop(self): """Final comprehensive security report""" print("\n" + "="*60) print("COMPREHENSIVE SECURITY HEADERS VALIDATION COMPLETE") print("="*60) print(f"Pages validated: {len(self.visited_pages)}") print(f"Total security issues: {len(self.security_issues)}") # Header statistics print(f"\nHEADER STATISTICS:") for header, count in self.header_stats.items(): print(f" {header}: {count}") # Page type breakdown print(f"\nPAGE TYPE BREAKDOWN:") for page_type, pages in self.page_classifications.items(): print(f" {page_type}: {len(pages)} pages") if self.security_issues: print(f"\nTOP SECURITY ISSUES:") for issue in self.security_issues[:10]: print(f"❌ [{issue['severity']}] {issue['description']}") print(f" Page: {issue['page']}") else: print("✅ All pages have proper security headers!") ``` ## Key Security Headers Validation Features 1. **Website Crawling**: Discovers and validates all pages automatically 2. **Configurable Requirements**: Simple configuration for required headers 3. **Pass/Fail Testing**: LoadForge integration with clear success/failure status 4. **Page Type Classification**: Different requirements for login, API, and admin pages 5. **Header Value Validation**: Checks not just presence but proper configuration 6. **Comprehensive Reporting**: Detailed statistics and issue classification ## Configuration Guide Edit the configuration at the top of the script to match your security requirements: * **Required Headers**: Set to `True` for headers that must be present * **Header Values**: Specify valid values for headers that need validation * **Page Types**: Define specific requirements for different page types * **HSTS Settings**: Configure minimum max-age and subdomain requirements This guide provides comprehensive security headers validation across your entire website with flexible configuration options. # Simple CMS API Testing Source: https://docs.loadforge.com/examples/ecommerce-business/cms-apis Basic CMS API testing for content management, publishing, and retrieval operations This guide shows how to test CMS (Content Management System) APIs with common operations like creating, updating, publishing, and retrieving content. ## Use Cases * Test content creation and management * Validate publishing workflows * Test content retrieval and search * Check user permissions and access control ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class CMSTestUser(HttpUser): def on_start(self): # Content types for testing self.content_types = ["article", "page", "blog_post", "product"] # Sample content data self.content_samples = { "article": { "title": "Sample Article", "content": "This is a sample article content for testing purposes.", "tags": ["test", "article", "sample"] }, "page": { "title": "Test Page", "content": "This is test page content with some sample text.", "slug": "test-page" }, "blog_post": { "title": "Test Blog Post", "content": "Sample blog post content for CMS testing.", "category": "Technology" } } # API endpoints self.cms_endpoints = { "content": "/api/content", "publish": "/api/content/{id}/publish", "search": "/api/content/search" } # Track created content for cleanup self.created_content = [] @task(4) def test_create_content(self): """Test creating new content""" content_type = random.choice(self.content_types) # Create content based on type if content_type in self.content_samples: content_data = self.content_samples[content_type].copy() else: content_data = { "title": f"Test {content_type.title()}", "content": f"Sample {content_type} content for testing." } content_data.update({ "type": content_type, "status": "draft", "author": "test_user" }) with self.client.post( self.cms_endpoints["content"], json=content_data, name="Create Content" ) as response: if response.status_code in [200, 201]: try: result = response.json() content_id = result.get("id") if content_id: self.created_content.append(content_id) print(f"Created {content_type}: {content_id}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Content creation failed: {response.status_code}") @task(3) def test_update_content(self): """Test updating existing content""" if not self.created_content: return content_id = random.choice(self.created_content) update_data = { "title": f"Updated Content {random.randint(100, 999)}", "content": "This content has been updated for testing purposes.", "last_modified": "2024-01-01T12:00:00Z" } with self.client.put( f"{self.cms_endpoints['content']}/{content_id}", json=update_data, name="Update Content" ) as response: if response.status_code == 200: print(f"Updated content: {content_id}") elif response.status_code == 404: print(f"Content not found: {content_id}") # Remove from our list if content_id in self.created_content: self.created_content.remove(content_id) else: response.failure(f"Content update failed: {response.status_code}") @task(3) def test_publish_content(self): """Test publishing content""" if not self.created_content: return content_id = random.choice(self.created_content) publish_data = { "status": "published", "publish_date": "2024-01-01T10:00:00Z" } publish_url = self.cms_endpoints["publish"].format(id=content_id) with self.client.post( publish_url, json=publish_data, name="Publish Content" ) as response: if response.status_code == 200: print(f"Published content: {content_id}") elif response.status_code == 404: print(f"Content not found for publishing: {content_id}") if content_id in self.created_content: self.created_content.remove(content_id) else: response.failure(f"Content publishing failed: {response.status_code}") @task(2) def test_retrieve_content(self): """Test retrieving content by ID""" if not self.created_content: return content_id = random.choice(self.created_content) with self.client.get( f"{self.cms_endpoints['content']}/{content_id}", name="Retrieve Content" ) as response: if response.status_code == 200: try: content = response.json() title = content.get("title", "Unknown") status = content.get("status", "Unknown") print(f"Retrieved content: {title} ({status})") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 404: print(f"Content not found: {content_id}") if content_id in self.created_content: self.created_content.remove(content_id) else: response.failure(f"Content retrieval failed: {response.status_code}") @task(2) def test_search_content(self): """Test content search functionality""" search_terms = ["test", "sample", "article", "content"] search_term = random.choice(search_terms) search_params = { "q": search_term, "limit": 10, "type": random.choice(self.content_types) } with self.client.get( self.cms_endpoints["search"], params=search_params, name="Search Content" ) as response: if response.status_code == 200: try: results = response.json() # Handle different response formats if isinstance(results, dict): items = results.get("results", results.get("data", [])) total = results.get("total", len(items)) else: items = results if isinstance(results, list) else [] total = len(items) print(f"Search '{search_term}': {len(items)} results") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Content search failed: {response.status_code}") @task(1) def test_list_content(self): """Test listing all content with pagination""" list_params = { "page": random.randint(1, 3), "per_page": 20, "status": random.choice(["all", "published", "draft"]) } with self.client.get( self.cms_endpoints["content"], params=list_params, name="List Content" ) as response: if response.status_code == 200: try: data = response.json() if isinstance(data, dict): items = data.get("content", data.get("data", [])) page = data.get("page", 1) total = data.get("total", len(items)) else: items = data if isinstance(data, list) else [] page = list_params["page"] total = len(items) print(f"Listed content: {len(items)} items, page {page}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Content listing failed: {response.status_code}") ``` ## Setup Instructions 1. Replace API endpoints with your actual CMS API URLs 2. Update content types to match your CMS content model 3. Adjust content data structure based on your CMS requirements 4. Configure authentication headers if required by your CMS API ## What This Tests * **Content Creation**: Tests creating different types of content * **Content Updates**: Tests updating existing content * **Publishing Workflow**: Tests content publishing functionality * **Content Retrieval**: Tests fetching content by ID * **Search Functionality**: Tests content search capabilities * **Content Listing**: Tests paginated content listing ## Best Practices * Use realistic content data that matches your content model * Test different content types and statuses * Validate both published and draft content workflows * Monitor API performance with different content volumes * Test search functionality with various terms ## Common Issues * **Content ID Management**: Track created content IDs for subsequent operations * **Status Workflows**: Understand your CMS's content status transitions * **Permissions**: Ensure test user has appropriate content management permissions * **Search Indexing**: Search results may have delays due to indexing # Simple Inventory API Testing Source: https://docs.loadforge.com/examples/ecommerce-business/inventory-apis Basic inventory API testing for stock management, updates, and availability checks This guide shows how to test inventory management APIs with common operations like checking stock, updating inventory, and managing product availability. ## Use Cases * Test inventory stock level checking * Validate inventory updates and adjustments * Test low stock alerts and notifications * Check inventory synchronization across systems ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class InventoryTestUser(HttpUser): def on_start(self): # Sample product data for testing self.products = [ {"id": "PROD001", "sku": "LAPTOP-HP-001", "name": "HP Laptop"}, {"id": "PROD002", "sku": "PHONE-IPH-001", "name": "iPhone 13"}, {"id": "PROD003", "sku": "HEADP-SON-001", "name": "Sony Headphones"}, {"id": "PROD004", "sku": "TABLET-IPD-001", "name": "iPad Pro"}, {"id": "PROD005", "sku": "WATCH-APL-001", "name": "Apple Watch"} ] # Inventory operations self.operations = ["restock", "adjustment", "sale", "return"] # API endpoints self.inventory_endpoints = { "check": "/api/inventory/check", "update": "/api/inventory/update", "bulk": "/api/inventory/bulk-update", "alerts": "/api/inventory/alerts" } @task(4) def test_check_inventory(self): """Test checking inventory levels""" product = random.choice(self.products) params = { "sku": product["sku"], "location": random.choice(["warehouse_1", "store_001", "online"]) } with self.client.get( self.inventory_endpoints["check"], params=params, name="Check Inventory" ) as response: if response.status_code == 200: try: inventory = response.json() available = inventory.get("available", 0) reserved = inventory.get("reserved", 0) total = inventory.get("total", 0) print(f"Inventory check {product['sku']}: {available}/{total} available") # Validate inventory data if available < 0: response.failure("Negative available inventory") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 404: print(f"Product not found: {product['sku']}") else: response.failure(f"Inventory check failed: {response.status_code}") @task(3) def test_update_inventory(self): """Test updating inventory levels""" product = random.choice(self.products) operation = random.choice(self.operations) # Generate appropriate quantity based on operation if operation == "restock": quantity = random.randint(10, 100) elif operation == "sale": quantity = -random.randint(1, 5) elif operation == "return": quantity = random.randint(1, 3) else: # adjustment quantity = random.randint(-10, 10) update_data = { "sku": product["sku"], "quantity": quantity, "operation": operation, "location": "warehouse_1", "reason": f"Test {operation} operation" } with self.client.post( self.inventory_endpoints["update"], json=update_data, name="Update Inventory" ) as response: if response.status_code == 200: try: result = response.json() new_quantity = result.get("new_quantity", 0) print(f"Inventory updated {product['sku']}: {operation} {quantity}, new total: {new_quantity}") # Validate update if new_quantity < 0 and operation != "adjustment": print(f"Warning: Negative inventory after {operation}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 400: print(f"Invalid inventory update: {operation} {quantity}") elif response.status_code == 409: print(f"Inventory conflict: insufficient stock for {operation}") else: response.failure(f"Inventory update failed: {response.status_code}") @task(2) def test_bulk_inventory_update(self): """Test bulk inventory updates""" # Select 2-3 products for bulk update selected_products = random.sample(self.products, random.randint(2, 3)) bulk_updates = [] for product in selected_products: bulk_updates.append({ "sku": product["sku"], "quantity": random.randint(5, 50), "operation": "restock", "location": "warehouse_1" }) bulk_data = { "updates": bulk_updates, "batch_id": f"BATCH_{random.randint(1000, 9999)}" } with self.client.post( self.inventory_endpoints["bulk"], json=bulk_data, name="Bulk Inventory Update" ) as response: if response.status_code == 200: try: result = response.json() processed = result.get("processed", 0) failed = result.get("failed", 0) batch_id = result.get("batch_id") print(f"Bulk update: {processed} processed, {failed} failed (batch: {batch_id})") if failed > 0: print(f"Some bulk updates failed: {failed}/{len(bulk_updates)}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Bulk inventory update failed: {response.status_code}") @task(2) def test_inventory_availability(self): """Test inventory availability checking""" product = random.choice(self.products) requested_quantity = random.randint(1, 10) availability_params = { "sku": product["sku"], "quantity": requested_quantity, "location": random.choice(["warehouse_1", "store_001", "all"]) } with self.client.get( f"{self.inventory_endpoints['check']}/availability", params=availability_params, name="Check Availability" ) as response: if response.status_code == 200: try: availability = response.json() available = availability.get("available", False) can_fulfill = availability.get("can_fulfill", 0) locations = availability.get("locations", []) print(f"Availability {product['sku']}: {can_fulfill}/{requested_quantity} available") if not available and can_fulfill > 0: print(f"Inconsistent availability data") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Availability check failed: {response.status_code}") @task(1) def test_low_stock_alerts(self): """Test low stock alert system""" with self.client.get( self.inventory_endpoints["alerts"], params={"threshold": random.choice([5, 10, 20])}, name="Low Stock Alerts" ) as response: if response.status_code == 200: try: alerts = response.json() if isinstance(alerts, dict): items = alerts.get("alerts", alerts.get("items", [])) else: items = alerts if isinstance(alerts, list) else [] print(f"Low stock alerts: {len(items)} products below threshold") # Validate alert data for alert in items[:3]: # Check first 3 alerts if isinstance(alert, dict): sku = alert.get("sku") current_stock = alert.get("current_stock", 0) threshold = alert.get("threshold", 0) if current_stock > threshold: print(f"Invalid alert: {sku} stock {current_stock} > threshold {threshold}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Low stock alerts failed: {response.status_code}") @task(1) def test_inventory_reservation(self): """Test inventory reservation for orders""" product = random.choice(self.products) reserve_quantity = random.randint(1, 5) reservation_data = { "sku": product["sku"], "quantity": reserve_quantity, "order_id": f"ORDER_{random.randint(10000, 99999)}", "expires_in": 900 # 15 minutes } with self.client.post( f"{self.inventory_endpoints['update']}/reserve", json=reservation_data, name="Reserve Inventory" ) as response: if response.status_code == 200: try: result = response.json() reservation_id = result.get("reservation_id") reserved_quantity = result.get("reserved_quantity", 0) expires_at = result.get("expires_at") print(f"Reserved {reserved_quantity} units of {product['sku']} (ID: {reservation_id})") # Test releasing the reservation if reservation_id: self._release_reservation(reservation_id) except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 409: print(f"Insufficient inventory to reserve {reserve_quantity} units") else: response.failure(f"Inventory reservation failed: {response.status_code}") def _release_reservation(self, reservation_id): """Helper method to release inventory reservation""" with self.client.delete( f"{self.inventory_endpoints['update']}/reserve/{reservation_id}", name="Release Reservation" ) as response: if response.status_code == 200: print(f"Released reservation: {reservation_id}") else: print(f"Failed to release reservation: {reservation_id}") ``` ## Setup Instructions 1. Replace inventory endpoints with your actual inventory API URLs 2. Update product data with real SKUs and product information 3. Adjust location names to match your warehouse/store setup 4. Configure authentication headers if required by your inventory API ## What This Tests * **Inventory Checking**: Tests stock level retrieval and availability * **Inventory Updates**: Tests stock adjustments and operations * **Bulk Operations**: Tests batch inventory updates * **Availability Checks**: Tests product availability validation * **Low Stock Alerts**: Tests inventory monitoring and alerts * **Reservations**: Tests inventory reservation and release ## Best Practices * Use realistic product SKUs and inventory operations * Test both positive and negative inventory adjustments * Validate inventory consistency across operations * Monitor performance with bulk operations * Test edge cases like negative inventory and conflicts ## Common Issues * **Concurrency**: Multiple updates to same SKU may cause conflicts * **Negative Inventory**: Some systems allow, others don't * **Location Sync**: Multi-location inventory may have sync delays * **Reservation Expiry**: Test reservation timeout handling ``` This guide provides comprehensive inventory management testing patterns for e-commerce and retail applications with proper stock tracking and warehouse operations. ``` # Simple Multi-Tenant API Testing Source: https://docs.loadforge.com/examples/ecommerce-business/multi-tenant-apis Basic multi-tenant API testing for tenant isolation, data segregation, and access control This guide shows how to test multi-tenant APIs with tenant-specific operations, data isolation, and cross-tenant access validation. ## Use Cases * Test tenant data isolation and segregation * Validate tenant-specific API access and permissions * Test tenant provisioning and configuration * Check cross-tenant security boundaries ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class MultiTenantTestUser(HttpUser): def on_start(self): # Sample tenant configurations self.tenants = [ {"id": "tenant001", "name": "Acme Corp", "plan": "enterprise", "subdomain": "acme"}, {"id": "tenant002", "name": "Beta LLC", "plan": "professional", "subdomain": "beta"}, {"id": "tenant003", "name": "Gamma Inc", "plan": "starter", "subdomain": "gamma"} ] # Select a tenant for this test session self.current_tenant = random.choice(self.tenants) self.tenant_id = self.current_tenant["id"] # API endpoints self.tenant_endpoints = { "data": "/api/tenants/{tenant_id}/data", "users": "/api/tenants/{tenant_id}/users", "config": "/api/tenants/{tenant_id}/config", "resources": "/api/tenants/{tenant_id}/resources" } # Authentication tokens (would be obtained via login in real scenario) self.tenant_tokens = { "tenant001": "token_acme_123", "tenant002": "token_beta_456", "tenant003": "token_gamma_789" } def get_tenant_headers(self, tenant_id=None): """Get headers with tenant-specific authentication""" if not tenant_id: tenant_id = self.tenant_id return { "Authorization": f"Bearer {self.tenant_tokens.get(tenant_id, 'invalid_token')}", "X-Tenant-ID": tenant_id, "Content-Type": "application/json" } @task(4) def test_tenant_data_access(self): """Test accessing tenant-specific data""" endpoint = self.tenant_endpoints["data"].format(tenant_id=self.tenant_id) headers = self.get_tenant_headers() with self.client.get( endpoint, headers=headers, name="Tenant Data Access" ) as response: if response.status_code == 200: try: data = response.json() items = data.get("data", data.get("items", [])) tenant_context = data.get("tenant_id") print(f"Tenant {self.tenant_id} data: {len(items)} items") # Validate tenant context if tenant_context and tenant_context != self.tenant_id: response.failure(f"Data leakage: Expected {self.tenant_id}, got {tenant_context}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 403: response.failure("Access denied to tenant data") else: response.failure(f"Tenant data access failed: {response.status_code}") @task(3) def test_tenant_user_management(self): """Test tenant-specific user operations""" endpoint = self.tenant_endpoints["users"].format(tenant_id=self.tenant_id) headers = self.get_tenant_headers() # Create a test user for this tenant user_data = { "username": f"testuser_{random.randint(1000, 9999)}", "email": f"test{random.randint(100, 999)}@{self.current_tenant['subdomain']}.example.com", "role": random.choice(["admin", "user", "viewer"]), "tenant_id": self.tenant_id } with self.client.post( endpoint, json=user_data, headers=headers, name="Create Tenant User" ) as response: if response.status_code in [200, 201]: try: result = response.json() user_id = result.get("user_id") or result.get("id") created_tenant = result.get("tenant_id") print(f"Created user {user_id} for tenant {self.tenant_id}") # Validate tenant assignment if created_tenant != self.tenant_id: response.failure(f"User created in wrong tenant: {created_tenant}") # Test retrieving the user if user_id: self._test_get_tenant_user(user_id) except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Create tenant user failed: {response.status_code}") def _test_get_tenant_user(self, user_id): """Helper method to test retrieving tenant user""" endpoint = f"{self.tenant_endpoints['users'].format(tenant_id=self.tenant_id)}/{user_id}" headers = self.get_tenant_headers() with self.client.get( endpoint, headers=headers, name="Get Tenant User" ) as response: if response.status_code == 200: try: user = response.json() user_tenant = user.get("tenant_id") if user_tenant != self.tenant_id: response.failure(f"User tenant mismatch: {user_tenant}") except json.JSONDecodeError: response.failure("Invalid JSON response") @task(2) def test_tenant_configuration(self): """Test tenant-specific configuration management""" endpoint = self.tenant_endpoints["config"].format(tenant_id=self.tenant_id) headers = self.get_tenant_headers() # Update tenant configuration config_data = { "features": { "advanced_analytics": self.current_tenant["plan"] == "enterprise", "api_rate_limit": 1000 if self.current_tenant["plan"] == "enterprise" else 100, "storage_limit_gb": 100 if self.current_tenant["plan"] == "enterprise" else 10 }, "branding": { "theme_color": f"#{random.randint(100000, 999999):06x}", "logo_url": f"https://{self.current_tenant['subdomain']}.example.com/logo.png" } } with self.client.put( endpoint, json=config_data, headers=headers, name="Update Tenant Config" ) as response: if response.status_code == 200: try: result = response.json() updated_tenant = result.get("tenant_id") print(f"Updated config for tenant {self.tenant_id}") if updated_tenant != self.tenant_id: response.failure(f"Config updated for wrong tenant: {updated_tenant}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Update tenant config failed: {response.status_code}") @task(2) def test_cross_tenant_access_denied(self): """Test that cross-tenant access is properly denied""" # Try to access a different tenant's data other_tenants = [t for t in self.tenants if t["id"] != self.tenant_id] if not other_tenants: return other_tenant = random.choice(other_tenants) other_tenant_id = other_tenant["id"] # Use current tenant's token to try accessing other tenant's data endpoint = self.tenant_endpoints["data"].format(tenant_id=other_tenant_id) headers = self.get_tenant_headers() # Uses current tenant's token with self.client.get( endpoint, headers=headers, name="Cross-Tenant Access Test" ) as response: if response.status_code == 403: print(f"Cross-tenant access correctly denied: {self.tenant_id} -> {other_tenant_id}") elif response.status_code == 404: print(f"Cross-tenant resource not found (acceptable): {other_tenant_id}") elif response.status_code == 200: response.failure(f"Security breach: Cross-tenant access allowed {self.tenant_id} -> {other_tenant_id}") else: print(f"Cross-tenant access returned: {response.status_code}") @task(1) def test_tenant_resource_limits(self): """Test tenant-specific resource limits""" endpoint = self.tenant_endpoints["resources"].format(tenant_id=self.tenant_id) headers = self.get_tenant_headers() with self.client.get( endpoint, headers=headers, name="Tenant Resource Limits" ) as response: if response.status_code == 200: try: resources = response.json() used_storage = resources.get("storage_used_gb", 0) storage_limit = resources.get("storage_limit_gb", 0) api_calls_used = resources.get("api_calls_used", 0) api_calls_limit = resources.get("api_calls_limit", 0) print(f"Tenant {self.tenant_id} resources: Storage {used_storage}/{storage_limit}GB, API {api_calls_used}/{api_calls_limit}") # Validate plan-based limits plan = self.current_tenant["plan"] expected_storage = 100 if plan == "enterprise" else 10 if storage_limit != expected_storage: print(f"Warning: Storage limit {storage_limit} doesn't match plan {plan}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Get tenant resources failed: {response.status_code}") @task(1) def test_tenant_subdomain_access(self): """Test tenant subdomain-based access""" subdomain = self.current_tenant["subdomain"] # Test subdomain-specific endpoint subdomain_endpoint = f"/api/subdomain/{subdomain}/info" headers = self.get_tenant_headers() with self.client.get( subdomain_endpoint, headers=headers, name="Subdomain Access" ) as response: if response.status_code == 200: try: info = response.json() returned_tenant = info.get("tenant_id") returned_subdomain = info.get("subdomain") print(f"Subdomain access: {subdomain} -> {returned_tenant}") if returned_tenant != self.tenant_id: response.failure(f"Subdomain mapped to wrong tenant: {returned_tenant}") if returned_subdomain != subdomain: response.failure(f"Subdomain mismatch: {returned_subdomain}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Subdomain access failed: {response.status_code}") ``` ## Setup Instructions 1. Replace tenant endpoints with your actual multi-tenant API URLs 2. Update tenant configurations with real tenant data 3. Configure authentication tokens for each tenant 4. Adjust plan-based feature validation for your SaaS model ## What This Tests * **Tenant Data Access**: Tests tenant-specific data retrieval and isolation * **User Management**: Tests tenant-scoped user operations * **Configuration**: Tests tenant-specific configuration management * **Cross-Tenant Security**: Tests that tenants cannot access each other's data * **Resource Limits**: Tests plan-based resource limits and quotas * **Subdomain Access**: Tests subdomain-based tenant routing ## Best Practices * Always validate tenant context in API responses * Test both positive and negative access scenarios * Verify plan-based feature restrictions * Test tenant isolation under concurrent load * Validate subdomain and routing mechanisms ## Common Issues * **Data Leakage**: Ensure no cross-tenant data is returned * **Authentication**: Verify tenant-specific token validation * **Resource Limits**: Plan upgrades/downgrades may not be immediate * **Subdomain Conflicts**: Handle subdomain uniqueness and conflicts # Simple Payment API Testing Source: https://docs.loadforge.com/examples/ecommerce-business/payment-apis Basic payment API testing with charge, refund, and validation scenarios This guide shows how to test payment APIs with common operations like charging, refunding, and validating payment methods. ## Use Cases * Test payment processing under load * Validate payment flow success and failure scenarios * Test refund processing * Check payment method validation ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class PaymentTestUser(HttpUser): def on_start(self): # Payment test data self.test_cards = [ {"number": "4242424242424242", "exp_month": 12, "exp_year": 2025, "cvc": "123"}, {"number": "4000000000000002", "exp_month": 11, "exp_year": 2025, "cvc": "456"}, # Declined card {"number": "4000000000009995", "exp_month": 10, "exp_year": 2025, "cvc": "789"} # Insufficient funds ] self.amounts = [999, 1500, 2999, 4999, 9999] # Amounts in cents self.currencies = ["usd", "eur", "gbp"] # API endpoints self.payment_endpoints = { "charge": "/api/payments/charge", "refund": "/api/payments/refund", "validate": "/api/payments/validate" } @task(4) def test_payment_charge(self): """Test payment charge with valid card""" card = self.test_cards[0] # Use valid card amount = random.choice(self.amounts) currency = random.choice(self.currencies) charge_data = { "amount": amount, "currency": currency, "card": card, "description": f"Test charge ${amount/100}" } with self.client.post( self.payment_endpoints["charge"], json=charge_data, name="Payment Charge" ) as response: if response.status_code == 200: try: result = response.json() if result.get("status") == "succeeded": print(f"Payment successful: ${amount/100} {currency}") else: print(f"Payment status: {result.get('status')}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 400: print("Payment failed: Bad request") elif response.status_code == 402: print("Payment failed: Card declined") else: response.failure(f"Payment charge failed: {response.status_code}") @task(2) def test_payment_declined(self): """Test payment with declined card""" declined_card = self.test_cards[1] # Declined card amount = random.choice(self.amounts) charge_data = { "amount": amount, "currency": "usd", "card": declined_card, "description": "Test declined payment" } with self.client.post( self.payment_endpoints["charge"], json=charge_data, name="Payment Declined" ) as response: if response.status_code == 402: print("Payment correctly declined") elif response.status_code == 200: try: result = response.json() if result.get("status") == "failed": print("Payment failed as expected") else: response.failure("Declined card was accepted") except json.JSONDecodeError: response.failure("Invalid JSON response") else: print(f"Declined payment returned: {response.status_code}") @task(2) def test_payment_refund(self): """Test payment refund""" # First create a charge, then refund it card = self.test_cards[0] amount = random.choice(self.amounts) # Create charge charge_data = { "amount": amount, "currency": "usd", "card": card, "description": "Test charge for refund" } with self.client.post( self.payment_endpoints["charge"], json=charge_data, name="Charge for Refund" ) as charge_response: if charge_response.status_code == 200: try: charge_result = charge_response.json() charge_id = charge_result.get("id") if charge_id: # Process refund refund_data = { "charge_id": charge_id, "amount": amount, "reason": "requested_by_customer" } with self.client.post( self.payment_endpoints["refund"], json=refund_data, name="Payment Refund" ) as refund_response: if refund_response.status_code == 200: print(f"Refund successful: ${amount/100}") else: response.failure(f"Refund failed: {refund_response.status_code}") else: print("No charge ID for refund") except json.JSONDecodeError: print("Invalid JSON in charge response") @task(2) def test_card_validation(self): """Test payment method validation""" card = random.choice(self.test_cards) validation_data = { "card": card, "validate_only": True } with self.client.post( self.payment_endpoints["validate"], json=validation_data, name="Card Validation" ) as response: if response.status_code == 200: try: result = response.json() is_valid = result.get("valid", False) card_type = result.get("card_type", "unknown") print(f"Card validation: {is_valid}, type: {card_type}") except json.JSONDecodeError: response.failure("Invalid JSON response") else: response.failure(f"Card validation failed: {response.status_code}") @task(1) def test_invalid_payment_data(self): """Test payment with invalid data""" invalid_scenarios = [ {"amount": -100, "currency": "usd", "description": "Negative amount"}, {"amount": 1000, "currency": "xxx", "description": "Invalid currency"}, {"amount": 0, "currency": "usd", "description": "Zero amount"} ] scenario = random.choice(invalid_scenarios) scenario["card"] = self.test_cards[0] with self.client.post( self.payment_endpoints["charge"], json=scenario, name="Invalid Payment Data" ) as response: if response.status_code == 400: print(f"Invalid data correctly rejected: {scenario['description']}") elif response.status_code == 200: response.failure(f"Invalid data accepted: {scenario['description']}") else: print(f"Invalid data returned: {response.status_code}") ``` ## Setup Instructions 1. Replace payment endpoints with your actual payment API URLs 2. Update test card numbers to match your payment provider's test cards 3. Adjust currency codes based on your supported currencies 4. Configure authentication headers if required by your payment API ## What This Tests * **Payment Processing**: Tests successful payment charges * **Decline Handling**: Tests proper handling of declined cards * **Refund Processing**: Tests refund functionality * **Card Validation**: Tests payment method validation * **Error Handling**: Tests invalid payment data scenarios ## Best Practices * Use test card numbers provided by your payment processor * Test different currencies and amounts * Validate both success and failure scenarios * Monitor payment processing times under load * Always test refund functionality ## Common Issues * **Test vs Live Keys**: Ensure you're using test API keys, not live ones * **Rate Limiting**: Payment APIs often have strict rate limits * **Currency Support**: Verify which currencies your API supports * **Webhook Delays**: Payment confirmations may be asynchronous # Simple Shopping Cart API Testing Source: https://docs.loadforge.com/examples/ecommerce-business/shopping-cart-apis Basic shopping cart API testing for add, update, remove, and checkout operations This guide shows how to test shopping cart APIs with common operations like adding items, updating quantities, and processing checkout. ## Use Cases * Test shopping cart item management * Validate cart persistence and session handling * Test checkout flow and cart validation * Check cart performance under concurrent access ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class ShoppingCartTestUser(HttpUser): def on_start(self): # Sample products for cart testing self.products = [ {"id": "PROD001", "name": "Laptop", "price": 999.99, "sku": "LAP001"}, {"id": "PROD002", "name": "Mouse", "price": 29.99, "sku": "MOU001"}, {"id": "PROD003", "name": "Keyboard", "price": 79.99, "sku": "KEY001"}, {"id": "PROD004", "name": "Monitor", "price": 299.99, "sku": "MON001"}, {"id": "PROD005", "name": "Headphones", "price": 149.99, "sku": "HEAD001"} ] # Cart and session management self.cart_id = None self.session_id = f"sess_{random.randint(100000, 999999)}" self.cart_items = [] # API endpoints self.cart_endpoints = { "create": "/api/cart", "add": "/api/cart/items", "update": "/api/cart/items/{item_id}", "remove": "/api/cart/items/{item_id}", "checkout": "/api/cart/checkout" } # Create a cart for this session self.create_cart() def create_cart(self): """Create a new shopping cart""" cart_data = { "session_id": self.session_id, "user_id": f"user_{random.randint(1000, 9999)}" } with self.client.post( self.cart_endpoints["create"], json=cart_data, name="Create Cart" ) as response: if response.status_code in [200, 201]: try: result = response.json() self.cart_id = result.get("cart_id") or result.get("id") print(f"Created cart: {self.cart_id}") except json.JSONDecodeError: print("Cart created but invalid JSON response") @task(4) def test_add_to_cart(self): """Test adding items to cart""" if not self.cart_id: self.create_cart() product = random.choice(self.products) quantity = random.randint(1, 3) item_data = { "cart_id": self.cart_id, "product_id": product["id"], "sku": product["sku"], "quantity": quantity, "price": product["price"] } with self.client.post( self.cart_endpoints["add"], json=item_data, name="Add to Cart" ) as response: if response.status_code in [200, 201]: try: result = response.json() item_id = result.get("item_id") or result.get("id") if item_id: self.cart_items.append({ "item_id": item_id, "product_id": product["id"], "quantity": quantity }) print(f"Added to cart: {quantity}x {product['name']}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 400: print(f"Invalid add to cart request") else: response.failure(f"Add to cart failed: {response.status_code}") @task(3) def test_update_cart_item(self): """Test updating cart item quantities""" if not self.cart_items: return cart_item = random.choice(self.cart_items) new_quantity = random.randint(1, 5) update_data = { "quantity": new_quantity } update_url = self.cart_endpoints["update"].format(item_id=cart_item["item_id"]) with self.client.put( update_url, json=update_data, name="Update Cart Item" ) as response: if response.status_code == 200: try: result = response.json() updated_quantity = result.get("quantity", new_quantity) # Update our local tracking cart_item["quantity"] = updated_quantity print(f"Updated cart item to quantity: {updated_quantity}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 404: print(f"Cart item not found: {cart_item['item_id']}") # Remove from our tracking self.cart_items.remove(cart_item) else: response.failure(f"Update cart item failed: {response.status_code}") @task(2) def test_remove_from_cart(self): """Test removing items from cart""" if not self.cart_items: return cart_item = random.choice(self.cart_items) remove_url = self.cart_endpoints["remove"].format(item_id=cart_item["item_id"]) with self.client.delete( remove_url, name="Remove from Cart" ) as response: if response.status_code in [200, 204]: print(f"Removed item from cart: {cart_item['item_id']}") self.cart_items.remove(cart_item) elif response.status_code == 404: print(f"Cart item already removed: {cart_item['item_id']}") self.cart_items.remove(cart_item) else: response.failure(f"Remove from cart failed: {response.status_code}") @task(2) def test_view_cart(self): """Test viewing cart contents""" if not self.cart_id: return params = {"cart_id": self.cart_id} with self.client.get( self.cart_endpoints["create"], params=params, name="View Cart" ) as response: if response.status_code == 200: try: cart = response.json() items = cart.get("items", []) total = cart.get("total", 0) item_count = cart.get("item_count", len(items)) print(f"Cart view: {item_count} items, total: ${total}") # Validate cart data consistency if len(items) != len(self.cart_items): print(f"Cart item count mismatch: API={len(items)}, local={len(self.cart_items)}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 404: print(f"Cart not found: {self.cart_id}") self.cart_id = None self.cart_items = [] else: response.failure(f"View cart failed: {response.status_code}") @task(1) def test_cart_checkout(self): """Test cart checkout process""" if not self.cart_items or not self.cart_id: return checkout_data = { "cart_id": self.cart_id, "shipping_address": { "street": "123 Test St", "city": "Test City", "state": "TS", "zip": "12345" }, "payment_method": { "type": "credit_card", "card_number": "4111111111111111", "exp_month": "12", "exp_year": "2025", "cvv": "123" } } with self.client.post( self.cart_endpoints["checkout"], json=checkout_data, name="Cart Checkout" ) as response: if response.status_code == 200: try: result = response.json() order_id = result.get("order_id") status = result.get("status", "unknown") total = result.get("total", 0) print(f"Checkout successful: Order {order_id}, status: {status}, total: ${total}") # Reset cart after successful checkout self.cart_items = [] self.cart_id = None except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 400: print("Checkout failed: Invalid cart or payment data") elif response.status_code == 409: print("Checkout failed: Cart conflict or inventory issue") else: response.failure(f"Cart checkout failed: {response.status_code}") @task(1) def test_cart_validation(self): """Test cart validation and error handling""" if not self.cart_id: return # Test adding invalid item invalid_item = { "cart_id": self.cart_id, "product_id": "INVALID_PRODUCT", "quantity": -1, # Invalid quantity "price": -10.00 # Invalid price } with self.client.post( self.cart_endpoints["add"], json=invalid_item, name="Invalid Cart Operation" ) as response: if response.status_code == 400: print("Invalid cart operation correctly rejected") elif response.status_code == 404: print("Invalid product correctly rejected") elif response.status_code == 200: response.failure("Invalid cart operation was accepted") else: print(f"Invalid cart operation returned: {response.status_code}") @task(1) def test_cart_persistence(self): """Test cart persistence across sessions""" if not self.cart_id: return # Simulate session interruption and recovery original_session = self.session_id # Try to recover cart with session ID recovery_params = { "session_id": original_session } with self.client.get( f"{self.cart_endpoints['create']}/recover", params=recovery_params, name="Recover Cart" ) as response: if response.status_code == 200: try: recovered_cart = response.json() recovered_id = recovered_cart.get("cart_id") if recovered_id == self.cart_id: print(f"Cart persistence verified: {recovered_id}") else: print(f"Cart recovery returned different ID: {recovered_id}") except json.JSONDecodeError: response.failure("Invalid JSON response") elif response.status_code == 404: print("No cart found for session recovery") else: print(f"Cart recovery returned: {response.status_code}") ``` ## Setup Instructions 1. Replace cart endpoints with your actual shopping cart API URLs 2. Update product data with real products from your catalog 3. Adjust cart data structure based on your cart API requirements 4. Configure authentication and session handling as needed ## What This Tests * **Cart Creation**: Tests creating new shopping carts * **Add to Cart**: Tests adding products to cart with quantities * **Cart Updates**: Tests updating item quantities in cart * **Item Removal**: Tests removing items from cart * **Cart Viewing**: Tests retrieving cart contents and totals * **Checkout Process**: Tests cart checkout and order creation * **Cart Validation**: Tests error handling for invalid operations * **Cart Persistence**: Tests cart recovery across sessions ## Best Practices * Test realistic shopping patterns and quantities * Validate cart totals and item counts * Test concurrent cart operations * Handle cart session management properly * Test both valid and invalid cart operations ## Common Issues * **Session Management**: Cart persistence across browser sessions * **Concurrency**: Multiple users updating same cart simultaneously * **Inventory Sync**: Cart items may become unavailable during checkout * **Price Updates**: Product prices may change while items are in cart # AI API Performance Comparison Source: https://docs.loadforge.com/examples/emerging-technology/ai-api-comparison Simple load testing to compare response times and quality across OpenAI, Claude, and Gemini APIs This guide shows how to compare AI API performance across different providers. Perfect for testing response times, reliability, and choosing the best AI provider for your needs. ## Use Cases * Compare AI API response times * Test different AI providers under load * Validate API reliability and uptime * Measure cost-effectiveness of different providers ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json import random import time class AIComparisonUser(HttpUser): def on_start(self): # API configurations self.openai_key = "your-openai-api-key" self.claude_key = "your-claude-api-key" self.gemini_key = "your-gemini-api-key" # Test prompts for comparison self.test_prompts = [ "Write a short product description for a wireless headphone.", "Explain quantum computing in simple terms.", "Create a brief email response thanking a customer.", "Summarize the benefits of renewable energy.", "Write a creative story opening in 2 sentences." ] @task(2) def test_openai_gpt35(self): """Test OpenAI GPT-3.5 Turbo""" prompt = random.choice(self.test_prompts) headers = { "Authorization": f"Bearer {self.openai_key}", "Content-Type": "application/json" } payload = { "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": prompt}], "max_tokens": 150, "temperature": 0.7 } start_time = time.time() with self.client.post( "https://api.openai.com/v1/chat/completions", json=payload, headers=headers, name="OpenAI GPT-3.5" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: data = response.json() if "choices" in data and data["choices"]: content = data["choices"][0]["message"]["content"] tokens = data.get("usage", {}).get("total_tokens", 0) print(f"OpenAI: {len(content)} chars, {tokens} tokens, {response_time:.0f}ms") else: response.failure("No choices in OpenAI response") elif response.status_code == 429: response.failure("OpenAI rate limit exceeded") else: response.failure(f"OpenAI error: {response.status_code}") @task(2) def test_claude(self): """Test Anthropic Claude""" prompt = random.choice(self.test_prompts) headers = { "x-api-key": self.claude_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01" } payload = { "model": "claude-3-haiku-20240307", "max_tokens": 150, "messages": [{"role": "user", "content": prompt}] } start_time = time.time() with self.client.post( "https://api.anthropic.com/v1/messages", json=payload, headers=headers, name="Claude Haiku" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: data = response.json() if "content" in data and data["content"]: content = data["content"][0]["text"] tokens = data.get("usage", {}).get("input_tokens", 0) + data.get("usage", {}).get("output_tokens", 0) print(f"Claude: {len(content)} chars, {tokens} tokens, {response_time:.0f}ms") else: response.failure("No content in Claude response") elif response.status_code == 429: response.failure("Claude rate limit exceeded") else: response.failure(f"Claude error: {response.status_code}") @task(2) def test_gemini(self): """Test Google Gemini""" prompt = random.choice(self.test_prompts) params = {"key": self.gemini_key} payload = { "contents": [{"parts": [{"text": prompt}]}], "generationConfig": { "maxOutputTokens": 150, "temperature": 0.7 } } start_time = time.time() with self.client.post( "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent", json=payload, params=params, name="Gemini Pro" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: data = response.json() if "candidates" in data and data["candidates"]: content = data["candidates"][0]["content"]["parts"][0]["text"] # Gemini doesn't return token count in basic API print(f"Gemini: {len(content)} chars, {response_time:.0f}ms") else: response.failure("No candidates in Gemini response") elif response.status_code == 429: response.failure("Gemini rate limit exceeded") else: response.failure(f"Gemini error: {response.status_code}") @task(1) def test_openai_gpt4(self): """Test OpenAI GPT-4 (if available)""" prompt = random.choice(self.test_prompts) headers = { "Authorization": f"Bearer {self.openai_key}", "Content-Type": "application/json" } payload = { "model": "gpt-4", "messages": [{"role": "user", "content": prompt}], "max_tokens": 150, "temperature": 0.7 } start_time = time.time() with self.client.post( "https://api.openai.com/v1/chat/completions", json=payload, headers=headers, name="OpenAI GPT-4" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: data = response.json() if "choices" in data and data["choices"]: content = data["choices"][0]["message"]["content"] tokens = data.get("usage", {}).get("total_tokens", 0) print(f"GPT-4: {len(content)} chars, {tokens} tokens, {response_time:.0f}ms") elif response.status_code == 429: response.failure("GPT-4 rate limit exceeded") elif response.status_code == 404: response.failure("GPT-4 not available (need API access)") else: response.failure(f"GPT-4 error: {response.status_code}") ``` ## Setup Instructions 1. Get API keys from each provider: * OpenAI: [platform.openai.com](https://platform.openai.com) * Claude: [console.anthropic.com](https://console.anthropic.com) * Gemini: [makersuite.google.com](https://makersuite.google.com) 2. Replace the API key placeholders with your actual keys 3. Start with low user counts to avoid hitting rate limits ## What This Tests * **Response Times**: Compare how fast each AI responds * **Rate Limits**: Test how each provider handles concurrent requests * **Reliability**: Check which APIs are most stable under load * **Output Quality**: Compare response length and coherence ## Performance Comparison Typical results you might see: * **GPT-3.5**: Fast responses (\~1-3 seconds), good for high volume * **Claude**: Moderate speed (\~2-4 seconds), excellent quality * **Gemini**: Variable speed (\~1-5 seconds), good for creative tasks * **GPT-4**: Slower (\~3-8 seconds), highest quality but expensive ## Rate Limits & Costs * **OpenAI**: Tier-based limits, pay per token * **Claude**: Message-based limits, pay per token * **Gemini**: Requests per minute limits, generous free tier ## Common Issues * **API Keys**: Ensure all keys are valid and have sufficient credits * **Rate Limits**: Each provider has different limits * **Model Availability**: GPT-4 requires special access # DeFi Protocol Testing Source: https://docs.loadforge.com/examples/emerging-technology/defi-protocol-testing Test DeFi protocols including DEX trading, lending platforms, yield farming, and liquidity pools with comprehensive transaction monitoring This guide demonstrates how to test DeFi (Decentralized Finance) protocols including DEX trading, lending platforms, yield farming, and liquidity management. Perfect for Web3 developers building DeFi applications, testing protocol integrations, and monitoring DeFi infrastructure performance. ## Use Cases * Testing DEX trading functionality (Uniswap, SushiSwap, PancakeSwap) * Validating lending and borrowing protocols (Aave, Compound, MakerDAO) * Monitoring yield farming and liquidity mining rewards * Testing flash loan functionality and arbitrage opportunities * Validating protocol governance and voting mechanisms * Load testing DeFi protocol APIs under high transaction volume ## Key Features * **Multi-Protocol Support**: Test various DeFi protocols with unified framework * **Transaction Monitoring**: Track transaction status, gas usage, and slippage * **Price Impact Analysis**: Monitor price impact and MEV opportunities * **Liquidity Pool Testing**: Test LP token operations and impermanent loss * **Gas Optimization**: Monitor and optimize gas usage across protocols * **Error Handling**: Robust handling of failed transactions and reverts ## Example Implementation ```python theme={null} from locust import task, HttpUser import json import time import random from web3 import Web3 from eth_account import Account class DeFiProtocolUser(HttpUser): abstract = True def on_start(self): # Web3 provider setup (using Infura/Alchemy) self.infura_url = "https://mainnet.infura.io/v3/your-project-id" self.w3 = Web3(Web3.HTTPProvider(self.infura_url)) # Test wallet setup (use test accounts only) self.test_private_key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" self.test_account = Account.from_key(self.test_private_key) self.wallet_address = self.test_account.address # Common DeFi contract addresses (Ethereum mainnet) self.contracts = { # Uniswap V3 "uniswap_v3_router": "0xE592427A0AEce92De3Edee1F18E0157C05861564", "uniswap_v3_factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984", "uniswap_v3_quoter": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", # Aave V3 "aave_pool": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", "aave_pool_data_provider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", # Compound V3 "compound_comet_usdc": "0xc3d688B66703497DAA19211EEdff47f25384cdc3", # Common ERC20 tokens "usdc": "0xA0b86a33E6441b8435b662303c0f479c7e1b5b8e", "usdt": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F" } # Common test amounts (in wei for ETH, smallest unit for tokens) self.test_amounts = { "small": int(0.01 * 10**18), # 0.01 ETH "medium": int(0.1 * 10**18), # 0.1 ETH "large": int(1 * 10**18) # 1 ETH } class UniswapV3User(DeFiProtocolUser): """Test Uniswap V3 DEX functionality""" @task(3) def test_uniswap_quote(self): """Test Uniswap V3 price quotes""" # Test WETH -> USDC quote token_in = self.contracts["weth"] token_out = self.contracts["usdc"] amount_in = self.test_amounts["small"] fee = 3000 # 0.3% fee tier # Quoter contract ABI (simplified) quoter_abi = [ { "inputs": [ {"name": "tokenIn", "type": "address"}, {"name": "tokenOut", "type": "address"}, {"name": "fee", "type": "uint24"}, {"name": "amountIn", "type": "uint256"}, {"name": "sqrtPriceLimitX96", "type": "uint160"} ], "name": "quoteExactInputSingle", "outputs": [{"name": "amountOut", "type": "uint256"}], "type": "function" } ] start_time = time.time() try: quoter_contract = self.w3.eth.contract( address=self.contracts["uniswap_v3_quoter"], abi=quoter_abi ) # Get quote amount_out = quoter_contract.functions.quoteExactInputSingle( token_in, token_out, fee, amount_in, 0 ).call() response_time = (time.time() - start_time) * 1000 if amount_out > 0: # Calculate price impact and slippage eth_price_estimate = amount_out / (amount_in / 10**18) / 10**6 # Rough USDC price # Log successful quote self.environment.events.request.fire( request_type="Uniswap_Quote", name=f"WETH->USDC Quote: ${eth_price_estimate:.2f}", start_time=start_time, response_time=response_time, response_length=len(str(amount_out)), context={ "token_pair": "WETH/USDC", "amount_in": amount_in, "amount_out": amount_out, "estimated_price": eth_price_estimate, "fee_tier": fee }, url="/uniswap/quote", exception=None, ) else: raise Exception("Invalid quote: amount_out is 0") except Exception as e: response_time = (time.time() - start_time) * 1000 self.environment.events.request.fire( request_type="Uniswap_Quote", name="WETH->USDC Quote Failed", start_time=start_time, response_time=response_time, response_length=0, context={"error": str(e)}, url="/uniswap/quote", exception=e, ) @task(2) def test_uniswap_pool_info(self): """Test Uniswap V3 pool information""" # WETH/USDC 0.3% pool pool_address = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8" # Pool contract ABI (simplified) pool_abi = [ { "inputs": [], "name": "slot0", "outputs": [ {"name": "sqrtPriceX96", "type": "uint160"}, {"name": "tick", "type": "int24"}, {"name": "observationIndex", "type": "uint16"}, {"name": "observationCardinality", "type": "uint16"}, {"name": "observationCardinalityNext", "type": "uint16"}, {"name": "feeProtocol", "type": "uint8"}, {"name": "unlocked", "type": "bool"} ], "type": "function" }, { "inputs": [], "name": "liquidity", "outputs": [{"name": "", "type": "uint128"}], "type": "function" } ] start_time = time.time() try: pool_contract = self.w3.eth.contract(address=pool_address, abi=pool_abi) # Get pool state slot0 = pool_contract.functions.slot0().call() liquidity = pool_contract.functions.liquidity().call() response_time = (time.time() - start_time) * 1000 sqrt_price = slot0[0] current_tick = slot0[1] # Calculate current price from sqrtPriceX96 price = (sqrt_price / (2**96))**2 self.environment.events.request.fire( request_type="Uniswap_Pool", name=f"WETH/USDC Pool Info - Tick: {current_tick}", start_time=start_time, response_time=response_time, response_length=len(str(liquidity)), context={ "pool_address": pool_address, "sqrt_price": sqrt_price, "current_tick": current_tick, "liquidity": liquidity, "calculated_price": price }, url="/uniswap/pool-info", exception=None, ) except Exception as e: response_time = (time.time() - start_time) * 1000 self.environment.events.request.fire( request_type="Uniswap_Pool", name="Pool Info Failed", start_time=start_time, response_time=response_time, response_length=0, context={"error": str(e)}, url="/uniswap/pool-info", exception=e, ) @task(1) def test_uniswap_transaction_simulation(self): ``` # DigitalOcean AI Performance Testing Source: https://docs.loadforge.com/examples/emerging-technology/digitalocean-ai-testing Load testing DigitalOcean's AI platform with Llama3 models to measure response times and reliability This guide shows how to performance test DigitalOcean's AI platform using Locust. Perfect for testing response times, reliability, and capacity planning for AI workloads on DigitalOcean. ## Use Cases * Test DigitalOcean AI API response times under load * Validate AI service reliability and uptime * Capacity planning for AI applications * Compare different Llama3 model variants * Monitor API rate limits and quotas ## Simple Implementation ```python theme={null} import random from locust import HttpUser, task, between class Llama3ChatUser(HttpUser): wait_time = between(1, 5) QUESTIONS = [ "What is the capital of France?", "Translate 'Hello, how are you?' into Spanish.", "Who wrote 'Pride and Prejudice'?", "What's 13 multiplied by 17?", "Name three benefits of a vegan diet.", "Give me a quick summary of the plot of '1984'.", "Explain the concept of machine learning in simple terms.", "What are the main differences between Python and JavaScript?", "How does photosynthesis work?", "What are some tips for effective time management?" ] @task def chat_completion(self): question = random.choice(self.QUESTIONS) # Used to show a preview of the question in LF preview = (question[:20] + "...") if len(question) > 20 else question payload = { "model": "llama3.3-70b-instruct", "messages": [{"role": "user", "content": question}], "stream": False, "include_functions_info": False, "include_retrieval_info": False, "include_guardrails_info": False } headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.api_token}" } with self.client.post( "/api/v1/chat/completions", json=payload, headers=headers, name=f"chat: {preview}", catch_response=True ) as response: if response.status_code == 200: response.success() else: response.failure(f"Status {response.status_code}") def on_start(self): # Set your DigitalOcean AI API token here self.api_token = "your-digitalocean-ai-token" ``` ## Setup Instructions 1. **Get DigitalOcean AI Access**: * Sign up for DigitalOcean account * Enable AI services in your project * Generate an API token from the control panel 2. **Configure the Script in LoadForge**: * Copy the script into LoadForge's test editor * Replace `your-digitalocean-ai-token` with your actual API token * Set the target host URL to your DigitalOcean AI endpoint 3. **Configure Load Test Settings**: * Start with 1-5 virtual users to test connectivity * Set appropriate ramp-up time to avoid rate limits * Monitor response times and error rates ## What This Tests * **Response Times**: Measure latency for Llama3.3 70B model * **Throughput**: Test concurrent request handling * **Rate Limits**: Understand DigitalOcean AI quotas and limits * **Reliability**: Check API stability under sustained load * **API Performance**: Validate DigitalOcean AI service quality ## Expected Performance Typical results for Llama 3.3 70B on DigitalOcean AI: * **Response Time**: \~3-6 seconds per request * **Quality**: High-quality responses with latest model improvements * **Throughput**: Suitable for production workloads with proper scaling ## Rate Limits & Pricing * **Request Limits**: Varies by plan and model * **Token Limits**: Based on input/output tokens * **Concurrent Requests**: Limited per account tier * **Pricing**: Pay-per-use model based on tokens consumed ## Common Issues * **Authentication**: Ensure API token has correct permissions * **Rate Limiting**: Start with low user counts to avoid 429 errors * **Endpoint URLs**: Verify the correct DigitalOcean AI endpoint * **Token Limits**: Monitor usage to avoid exceeding quotas ## Best Practices * **Gradual Ramp-up**: Start with 1-5 users, increase gradually * **Monitor Costs**: Track token usage to avoid unexpected charges * **Error Handling**: Implement proper retry logic for production use * **Caching**: Consider caching responses for repeated queries # Infura Node Testing Source: https://docs.loadforge.com/examples/emerging-technology/infura-node-testing Simple load testing for Infura blockchain node endpoints with basic RPC calls This guide shows how to load test Infura blockchain node endpoints with basic RPC calls. Perfect for testing your Infura project's performance and rate limits. ## Use Cases * Test Infura endpoint response times * Validate RPC call reliability * Check rate limit behavior * Monitor blockchain data retrieval performance ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json import random class InfuraUser(HttpUser): def on_start(self): # Replace with your Infura project ID self.infura_project_id = "your-project-id-here" self.rpc_url = f"https://mainnet.infura.io/v3/{self.infura_project_id}" # Test addresses for balance checks self.test_addresses = [ "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", # Vitalik "0xA0b86a33E6441b8435b662303c0f479c7e1b5b8e", # USDC "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # WETH ] @task(3) def get_latest_block(self): """Get the latest block number""" payload = { "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 } with self.client.post( self.rpc_url, json=payload, headers={"Content-Type": "application/json"}, name="Get Latest Block" ) as response: if response.status_code == 200: data = response.json() if "result" in data: block_number = int(data["result"], 16) print(f"Latest block: {block_number}") else: response.failure("No result in response") @task(2) def get_gas_price(self): """Get current gas price""" payload = { "jsonrpc": "2.0", "method": "eth_gasPrice", "params": [], "id": 1 } with self.client.post( self.rpc_url, json=payload, headers={"Content-Type": "application/json"}, name="Get Gas Price" ) as response: if response.status_code == 200: data = response.json() if "result" in data: gas_price = int(data["result"], 16) / 1e9 # Convert to Gwei print(f"Gas price: {gas_price:.2f} Gwei") @task(2) def check_balance(self): """Check ETH balance for random address""" address = random.choice(self.test_addresses) payload = { "jsonrpc": "2.0", "method": "eth_getBalance", "params": [address, "latest"], "id": 1 } with self.client.post( self.rpc_url, json=payload, headers={"Content-Type": "application/json"}, name="Check Balance" ) as response: if response.status_code == 200: data = response.json() if "result" in data: balance = int(data["result"], 16) / 1e18 # Convert to ETH print(f"Balance for {address}: {balance:.4f} ETH") @task(1) def get_block_details(self): """Get details of the latest block""" payload = { "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["latest", False], "id": 1 } with self.client.post( self.rpc_url, json=payload, headers={"Content-Type": "application/json"}, name="Get Block Details" ) as response: if response.status_code == 200: data = response.json() if "result" in data and data["result"]: block = data["result"] tx_count = len(block.get("transactions", [])) print(f"Block {block.get('number')} has {tx_count} transactions") ``` ## Setup Instructions 1. Sign up for a free Infura account at [infura.io](https://infura.io) 2. Create a new project and get your Project ID 3. Replace `your-project-id-here` with your actual Infura Project ID 4. Run the test to monitor your Infura endpoint performance ## What This Tests * **RPC Response Times**: Measures how fast Infura responds to different calls * **Rate Limiting**: Tests how Infura handles multiple concurrent requests * **Data Accuracy**: Validates that blockchain data is returned correctly * **Endpoint Reliability**: Checks for any connection issues or timeouts ## Common Issues * **Rate Limits**: Free Infura accounts have daily request limits * **Network Delays**: Blockchain networks can have variable response times * **Invalid Responses**: Always check for "result" field in JSON responses # Ollama Local AI Testing Source: https://docs.loadforge.com/examples/emerging-technology/ollama-testing Simple load testing for local Ollama AI models with text generation and chat completion This guide shows how to load test local Ollama AI models. Perfect for testing your self-hosted AI setup and comparing local vs cloud AI performance. ## Use Cases * Test local Ollama model performance * Compare different local AI models * Validate self-hosted AI reliability * Measure local AI response times ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json import random class OllamaUser(HttpUser): def on_start(self): # Ollama typically runs on localhost:11434 self.base_url = "http://localhost:11434" # Available models (install with: ollama pull model-name) self.models = [ "llama2:7b", "mistral:7b", "codellama:7b", "phi:2.7b" ] # Test prompts self.prompts = [ "Write a short product description for a smartphone.", "Explain machine learning in simple terms.", "Create a brief email to schedule a meeting.", "Write a Python function to calculate fibonacci numbers.", "Describe the benefits of renewable energy." ] @task(3) def generate_text(self): """Generate text using Ollama""" model = random.choice(self.models) prompt = random.choice(self.prompts) payload = { "model": model, "prompt": prompt, "stream": False, "options": { "temperature": 0.7, "num_predict": 100 } } with self.client.post( f"{self.base_url}/api/generate", json=payload, name=f"Generate - {model}" ) as response: if response.status_code == 200: try: data = response.json() response_text = data.get("response", "") done = data.get("done", False) if done and response_text: print(f"{model}: Generated {len(response_text)} characters") else: response.failure("Incomplete response from Ollama") except json.JSONDecodeError: response.failure("Invalid JSON response from Ollama") else: response.failure(f"Ollama error: {response.status_code}") @task(2) def chat_completion(self): """Test chat completion with Ollama""" model = random.choice(self.models) prompt = random.choice(self.prompts) payload = { "model": model, "messages": [ {"role": "user", "content": prompt} ], "stream": False, "options": { "temperature": 0.7, "num_predict": 100 } } with self.client.post( f"{self.base_url}/api/chat", json=payload, name=f"Chat - {model}" ) as response: if response.status_code == 200: try: data = response.json() message = data.get("message", {}) content = message.get("content", "") done = data.get("done", False) if done and content: print(f"{model} Chat: {len(content)} characters") else: response.failure("Incomplete chat response") except json.JSONDecodeError: response.failure("Invalid JSON response from Ollama") else: response.failure(f"Ollama chat error: {response.status_code}") @task(1) def list_models(self): """List available models""" with self.client.get( f"{self.base_url}/api/tags", name="List Models" ) as response: if response.status_code == 200: try: data = response.json() models = data.get("models", []) print(f"Available models: {len(models)}") for model in models[:3]: # Show first 3 name = model.get("name", "Unknown") size = model.get("size", 0) / (1024**3) # Convert to GB print(f" - {name}: {size:.1f}GB") except json.JSONDecodeError: response.failure("Invalid JSON response from Ollama") else: response.failure(f"Failed to list models: {response.status_code}") @task(1) def model_info(self): """Get information about a specific model""" model = random.choice(self.models) payload = {"name": model} with self.client.post( f"{self.base_url}/api/show", json=payload, name=f"Model Info - {model}" ) as response: if response.status_code == 200: try: data = response.json() modelfile = data.get("modelfile", "") parameters = data.get("parameters", "") print(f"{model} info retrieved") except json.JSONDecodeError: response.failure("Invalid JSON response from Ollama") else: response.failure(f"Failed to get model info: {response.status_code}") ``` ## Setup Instructions 1. **Install Ollama**: Download from [ollama.ai](https://ollama.ai) 2. **Pull Models**: Install models you want to test: ```bash theme={null} ollama pull llama2:7b ollama pull mistral:7b ollama pull phi:2.7b ``` 3. **Start Ollama**: Run `ollama serve` (usually starts automatically) 4. **Verify Setup**: Test with `curl http://localhost:11434/api/tags` ## What This Tests * **Local AI Performance**: Measures response times for local models * **Model Comparison**: Compare different models on same hardware * **Resource Usage**: Monitor CPU/GPU usage during testing * **Reliability**: Test local AI stability under load ## Performance Tips * **GPU Acceleration**: Use NVIDIA GPU for faster inference * **Model Size**: Smaller models (7B) are faster than larger ones (13B, 70B) * **Memory**: Ensure sufficient RAM for model loading * **Concurrent Users**: Start with low numbers to avoid overwhelming local hardware ## Common Issues * **Model Not Found**: Ensure models are pulled with `ollama pull` * **Connection Refused**: Check if Ollama service is running * **Slow Responses**: Local models are slower than cloud APIs * **Memory Issues**: Large models require significant RAM/VRAM # OpenSea NFT API Testing Source: https://docs.loadforge.com/examples/emerging-technology/opensea-nft-testing Simple load testing for OpenSea NFT marketplace API with collection stats and asset listings This guide shows how to load test OpenSea's NFT marketplace API. Perfect for testing NFT collection data retrieval and marketplace performance. ## Use Cases * Test OpenSea API response times * Monitor NFT collection statistics * Validate asset listing data * Check API rate limits and reliability ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random class OpenSeaUser(HttpUser): def on_start(self): # OpenSea API base URL self.base_url = "https://api.opensea.io/api/v1" # Popular NFT collections for testing self.collections = [ "boredapeyachtclub", "mutant-ape-yacht-club", "cryptopunks", "azuki", "doodles-official", "clonex", "proof-moonbirds" ] # API headers (no key required for basic endpoints) self.headers = { "Accept": "application/json", "User-Agent": "LoadForge-Test" } @task(3) def get_collection_stats(self): """Get statistics for a random NFT collection""" collection = random.choice(self.collections) with self.client.get( f"{self.base_url}/collection/{collection}/stats", headers=self.headers, name="Collection Stats" ) as response: if response.status_code == 200: data = response.json() stats = data.get("stats", {}) floor_price = stats.get("floor_price", 0) total_supply = stats.get("total_supply", 0) print(f"{collection}: Floor {floor_price} ETH, Supply {total_supply}") elif response.status_code == 429: response.failure("Rate limited by OpenSea") else: response.failure(f"Failed to get stats: {response.status_code}") @task(2) def get_collection_info(self): """Get basic information about a collection""" collection = random.choice(self.collections) with self.client.get( f"{self.base_url}/collection/{collection}", headers=self.headers, name="Collection Info" ) as response: if response.status_code == 200: data = response.json() collection_data = data.get("collection", {}) name = collection_data.get("name", "Unknown") description = collection_data.get("description", "")[:100] print(f"Collection: {name} - {description}...") elif response.status_code == 429: response.failure("Rate limited by OpenSea") @task(2) def get_assets(self): """Get assets from a random collection""" collection = random.choice(self.collections) params = { "collection": collection, "limit": 20, "offset": random.randint(0, 100) } with self.client.get( f"{self.base_url}/assets", params=params, headers=self.headers, name="Collection Assets" ) as response: if response.status_code == 200: data = response.json() assets = data.get("assets", []) print(f"Retrieved {len(assets)} assets from {collection}") # Check first asset details if assets: asset = assets[0] token_id = asset.get("token_id") name = asset.get("name", "Unnamed") print(f"First asset: {name} (#{token_id})") elif response.status_code == 429: response.failure("Rate limited by OpenSea") @task(1) def get_single_asset(self): """Get details for a specific NFT asset""" collection = random.choice(self.collections) # Use common token IDs that likely exist token_id = random.randint(1, 1000) # For this example, we'll use a known contract address (BAYC) contract_address = "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D" with self.client.get( f"{self.base_url}/asset/{contract_address}/{token_id}", headers=self.headers, name="Single Asset" ) as response: if response.status_code == 200: data = response.json() name = data.get("name", "Unnamed") description = data.get("description", "")[:50] print(f"Asset: {name} - {description}...") elif response.status_code == 404: # This is expected for non-existent tokens pass elif response.status_code == 429: response.failure("Rate limited by OpenSea") @task(1) def search_collections(self): """Search for collections by keyword""" search_terms = ["ape", "punk", "cat", "dog", "art", "pixel"] term = random.choice(search_terms) params = { "q": term, "limit": 10 } with self.client.get( f"{self.base_url}/collections", params=params, headers=self.headers, name="Search Collections" ) as response: if response.status_code == 200: data = response.json() collections = data.get("collections", []) print(f"Found {len(collections)} collections for '{term}'") elif response.status_code == 429: response.failure("Rate limited by OpenSea") ``` ## Setup Instructions 1. No API key required for basic OpenSea endpoints 2. Be mindful of rate limits (OpenSea limits requests per minute) 3. Consider using OpenSea API key for higher rate limits in production 4. Test with popular collections that are likely to have data ## What This Tests * **API Response Times**: Measures OpenSea API performance * **Rate Limiting**: Tests how the API handles concurrent requests * **Data Availability**: Validates that NFT data is accessible * **Search Functionality**: Tests collection and asset search features ## Rate Limits * OpenSea has strict rate limits (around 4 requests per second) * Use delays between requests for sustained testing * Consider upgrading to API key for higher limits * Monitor for 429 (Too Many Requests) responses ## Common Issues * **Rate Limiting**: OpenSea aggressively rate limits requests * **Data Inconsistency**: NFT data can change frequently * **Asset Availability**: Some assets may be private or removed # Uniswap DeFi Testing Source: https://docs.loadforge.com/examples/emerging-technology/uniswap-defi-testing Simple load testing for Uniswap decentralized exchange with price quotes and pool data This guide shows how to load test Uniswap DeFi protocol endpoints. Perfect for testing DEX price data, pool information, and DeFi protocol reliability. ## Use Cases * Test Uniswap API response times * Monitor DEX price quote accuracy * Validate pool liquidity data * Check DeFi protocol availability ## Simple Implementation ```python theme={null} from locust import task, HttpUser import json import random class UniswapUser(HttpUser): def on_start(self): # Uniswap V3 Subgraph endpoint self.subgraph_url = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3" # Popular trading pairs for testing self.token_pairs = [ {"token0": "WETH", "token1": "USDC", "address": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8"}, {"token0": "WETH", "token1": "USDT", "address": "0x4e68ccd3e89f51c3074ca5072bbac773960dfa36"}, {"token0": "WETH", "token1": "DAI", "address": "0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"}, {"token0": "USDC", "token1": "USDT", "address": "0x3416cf6c708da44db2624d63ea0aaef7113527c6"} ] # Common token addresses self.tokens = { "WETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "USDC": "0xa0b86a33e6441b8435b662303c0f479c7e1b5b8e", "USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7", "DAI": "0x6b175474e89094c44da98b954eedeac495271d0f" } @task(3) def get_pool_data(self): """Get pool information from Uniswap""" pair = random.choice(self.token_pairs) query = """ { pool(id: "%s") { id token0 { symbol decimals } token1 { symbol decimals } feeTier liquidity sqrtPrice tick token0Price token1Price volumeUSD txCount } } """ % pair["address"].lower() payload = {"query": query} with self.client.post( self.subgraph_url, json=payload, name="Pool Data" ) as response: if response.status_code == 200: try: data = response.json() if "data" in data and data["data"]["pool"]: pool = data["data"]["pool"] token0_symbol = pool["token0"]["symbol"] token1_symbol = pool["token1"]["symbol"] price = float(pool["token0Price"]) volume = float(pool["volumeUSD"]) print(f"{token0_symbol}/{token1_symbol}: Price {price:.6f}, Volume ${volume:,.0f}") else: response.failure("No pool data returned") except (json.JSONDecodeError, KeyError, ValueError) as e: response.failure(f"Failed to parse pool data: {e}") else: response.failure(f"Subgraph error: {response.status_code}") @task(2) def get_token_prices(self): """Get current token prices""" query = """ { tokens(first: 5, orderBy: volumeUSD, orderDirection: desc) { id symbol name decimals derivedETH volumeUSD txCount } } """ payload = {"query": query} with self.client.post( self.subgraph_url, json=payload, name="Token Prices" ) as response: if response.status_code == 200: try: data = response.json() if "data" in data and "tokens" in data["data"]: tokens = data["data"]["tokens"] for token in tokens[:3]: # Show top 3 symbol = token["symbol"] price_eth = float(token["derivedETH"]) volume = float(token["volumeUSD"]) print(f"{symbol}: {price_eth:.6f} ETH, Volume ${volume:,.0f}") else: response.failure("No token data returned") except (json.JSONDecodeError, KeyError, ValueError) as e: response.failure(f"Failed to parse token data: {e}") else: response.failure(f"Subgraph error: {response.status_code}") @task(2) def get_recent_swaps(self): """Get recent swap transactions""" query = """ { swaps(first: 10, orderBy: timestamp, orderDirection: desc) { id timestamp pool { token0 { symbol } token1 { symbol } } amount0 amount1 amountUSD sender } } """ payload = {"query": query} with self.client.post( self.subgraph_url, json=payload, name="Recent Swaps" ) as response: if response.status_code == 200: try: data = response.json() if "data" in data and "swaps" in data["data"]: swaps = data["data"]["swaps"] print(f"Retrieved {len(swaps)} recent swaps") if swaps: swap = swaps[0] # Show most recent token0 = swap["pool"]["token0"]["symbol"] token1 = swap["pool"]["token1"]["symbol"] amount_usd = float(swap["amountUSD"]) print(f"Latest swap: {token0}/{token1} - ${amount_usd:,.2f}") else: response.failure("No swap data returned") except (json.JSONDecodeError, KeyError, ValueError) as e: response.failure(f"Failed to parse swap data: {e}") else: response.failure(f"Subgraph error: {response.status_code}") @task(1) def get_pool_stats(self): """Get overall Uniswap statistics""" query = """ { uniswapDayDatas(first: 1, orderBy: date, orderDirection: desc) { date volumeUSD tvlUSD txCount } } """ payload = {"query": query} with self.client.post( self.subgraph_url, json=payload, name="Pool Stats" ) as response: if response.status_code == 200: try: data = response.json() if "data" in data and "uniswapDayDatas" in data["data"]: day_data = data["data"]["uniswapDayDatas"] if day_data: stats = day_data[0] volume = float(stats["volumeUSD"]) tvl = float(stats["tvlUSD"]) tx_count = int(stats["txCount"]) print(f"Daily Volume: ${volume:,.0f}, TVL: ${tvl:,.0f}, Txs: {tx_count:,}") else: response.failure("No stats data returned") except (json.JSONDecodeError, KeyError, ValueError) as e: response.failure(f"Failed to parse stats data: {e}") else: response.failure(f"Subgraph error: {response.status_code}") ``` ## Setup Instructions 1. No API key required for The Graph public endpoints 2. Uniswap V3 subgraph provides real-time DEX data 3. Test with popular trading pairs for reliable data 4. Monitor for rate limits on The Graph ## What This Tests * **Subgraph Performance**: Tests The Graph indexing service * **DeFi Data Accuracy**: Validates pool and price data * **Real-time Updates**: Checks how current the data is * **Query Complexity**: Tests different GraphQL query types ## Key Metrics * **Pool Liquidity**: Total value locked in trading pairs * **Trading Volume**: 24h volume for pools and tokens * **Price Data**: Current exchange rates between tokens * **Transaction Count**: Number of swaps and interactions ## Common Issues * **Subgraph Delays**: Data may be slightly behind blockchain * **Rate Limits**: The Graph has query limits * **Pool Addresses**: Ensure correct pool contract addresses * **Price Volatility**: DeFi prices change rapidly ## DeFi Concepts * **TVL**: Total Value Locked in the protocol * **Liquidity**: Available tokens for trading * **Slippage**: Price impact of large trades * **Fees**: Trading fees collected by liquidity providers # Localization & Internationalization Testing Source: https://docs.loadforge.com/examples/geographic-localization/localization-internationalization Test your application's localization (l10n) and internationalization (i18n) features across different languages, regions, and cultural contexts Test your application's localization (l10n) and internationalization (i18n) features across different languages, regions, and cultural contexts. ## Use Cases * **Multi-Language Support**: Test language switching and content translation * **Currency Localization**: Validate currency formatting and conversion * **Date/Time Formatting**: Test regional date and time display formats * **Cultural Adaptation**: Validate region-specific content and behavior * **RTL Language Support**: Test right-to-left language rendering ## Locustfile ```python theme={null} from locust import HttpUser, task, between import random import json class LocalizationUser(HttpUser): """Test localization and internationalization features""" wait_time = between(1, 3) def on_start(self): """Initialize localization testing configuration""" self.locales = { 'en-US': {'language': 'English', 'currency': 'USD', 'rtl': False}, 'en-GB': {'language': 'English', 'currency': 'GBP', 'rtl': False}, 'de-DE': {'language': 'German', 'currency': 'EUR', 'rtl': False}, 'fr-FR': {'language': 'French', 'currency': 'EUR', 'rtl': False}, 'ja-JP': {'language': 'Japanese', 'currency': 'JPY', 'rtl': False}, 'ar-SA': {'language': 'Arabic', 'currency': 'SAR', 'rtl': True} } self.current_locale = random.choice(list(self.locales.keys())) self.locale_config = self.locales[self.current_locale] self.client.headers.update({ 'Accept-Language': self.current_locale, 'X-Locale': self.current_locale, 'X-Currency': self.locale_config['currency'] }) print(f"User initialized with locale: {self.current_locale}") @task(3) def test_language_content(self): """Test language-specific content delivery""" endpoints = ['/api/v1/content/homepage', '/api/v1/content/navigation'] endpoint = random.choice(endpoints) with self.client.get( endpoint, name=f"Language Content - {self.current_locale}", catch_response=True ) as response: if response.status_code == 200: try: content = response.json() if 'language' in content and content['language'] == self.current_locale: print(f"Correct language content: {self.current_locale}") response.success() else: response.success() # Content might not include language field except json.JSONDecodeError: response.success() else: response.failure(f"Language content failed: {response.status_code}") @task(3) def test_currency_formatting(self): """Test currency formatting and conversion""" with self.client.get( '/api/v1/pricing/products', name=f"Currency Format - {self.current_locale}", catch_response=True ) as response: if response.status_code == 200: try: pricing_data = response.json() expected_currency = self.locale_config['currency'] if 'currency' in pricing_data: if pricing_data['currency'] == expected_currency: print(f"Correct currency: {expected_currency}") response.success() else: response.failure(f"Wrong currency: expected {expected_currency}") else: response.success() except json.JSONDecodeError: response.failure("Invalid JSON in pricing data") else: response.failure(f"Currency formatting failed: {response.status_code}") @task(2) def test_locale_switching(self): """Test dynamic locale switching""" available_locales = list(self.locales.keys()) new_locale = random.choice([loc for loc in available_locales if loc != self.current_locale]) switch_data = {'locale': new_locale, 'current_locale': self.current_locale} with self.client.post( '/api/v1/user/locale', json=switch_data, name=f"Locale Switch - {new_locale}", catch_response=True ) as response: if response.status_code == 200: try: result = response.json() if result.get('locale') == new_locale: print(f"Successfully switched locale to {new_locale}") response.success() else: response.failure("Locale switch failed") except json.JSONDecodeError: response.failure("Invalid locale switch response") else: response.failure(f"Locale switch failed: {response.status_code}") @task(1) def test_rtl_language_support(self): """Test right-to-left language support""" if self.locale_config['rtl']: with self.client.get( '/api/v1/content/rtl-test', name=f"RTL Support - {self.current_locale}", catch_response=True ) as response: if response.status_code == 200: content_str = str(response.content) rtl_indicators = ['dir="rtl"', 'text-align: right'] has_rtl = any(indicator in content_str for indicator in rtl_indicators) if has_rtl: print(f"RTL support detected for {self.current_locale}") response.success() else: response.failure(f"RTL test failed: {response.status_code}") ``` ## Configuration Update these settings for your localization testing: ```python theme={null} # Supported Locales SUPPORTED_LOCALES = ['en-US', 'en-GB', 'de-DE', 'fr-FR', 'ja-JP', 'ar-SA'] # Currency Settings CURRENCY_MAPPING = { 'en-US': 'USD', 'en-GB': 'GBP', 'de-DE': 'EUR', 'fr-FR': 'EUR', 'ja-JP': 'JPY', 'ar-SA': 'SAR' } # RTL Languages RTL_LANGUAGES = ['ar-SA', 'he-IL', 'fa-IR'] ``` ## LoadForge Multi-Region Testing Use LoadForge's region selection to test localization from appropriate geographic locations: * **US Regions**: Test en-US localization * **European Regions**: Test European locales (en-GB, de-DE, fr-FR) * **Asian Regions**: Test Asian locales (ja-JP, zh-CN) ## Key Validation Points * **Language Content**: Verify translated content delivery * **Currency Formatting**: Validate currency symbols and number formatting * **Date/Time Display**: Check regional date and time formats * **RTL Support**: Test right-to-left language rendering * **Cultural Adaptation**: Validate region-specific features ## Troubleshooting * **Missing Translations**: Check translation file completeness * **Currency Errors**: Verify exchange rate APIs and formatting * **RTL Issues**: Review CSS and layout for RTL languages This locustfile ensures your application works correctly across different languages, regions, and cultural contexts. # Multi-Region Latency Testing Source: https://docs.loadforge.com/examples/geographic-localization/multi-region-latency-testing Test your application's performance across different geographic regions to ensure consistent user experience worldwide Test your application's performance across different geographic regions to ensure consistent user experience worldwide. This locustfile simulates users from various global locations accessing your services. ## Use Cases * **Global CDN Performance**: Test content delivery across regions * **API Latency Validation**: Measure response times from different locations * **Regional Failover Testing**: Validate geographic redundancy * **User Experience Consistency**: Ensure performance parity across regions * **Network Path Optimization**: Identify routing inefficiencies ## Locustfile ```python theme={null} from locust import HttpUser, task, between import random import time import json from datetime import datetime, timezone class MultiRegionUser(HttpUser): """Simulate users from different geographic regions""" wait_time = between(1, 3) def on_start(self): """Initialize region-specific configuration""" # Simulate different regional characteristics self.region_profiles = { 'us_east': { 'timezone': 'America/New_York', 'language': 'en-US', 'currency': 'USD', 'expected_latency': 50, # ms baseline 'peak_hours': [9, 10, 11, 14, 15, 16] }, 'us_west': { 'timezone': 'America/Los_Angeles', 'language': 'en-US', 'currency': 'USD', 'expected_latency': 80, 'peak_hours': [9, 10, 11, 14, 15, 16] }, 'europe': { 'timezone': 'Europe/London', 'language': 'en-GB', 'currency': 'EUR', 'expected_latency': 120, 'peak_hours': [8, 9, 10, 13, 14, 15] }, 'asia_pacific': { 'timezone': 'Asia/Singapore', 'language': 'en-SG', 'currency': 'SGD', 'expected_latency': 200, 'peak_hours': [9, 10, 11, 14, 15, 16] } } # Select random region profile self.current_region = random.choice(list(self.region_profiles.keys())) self.profile = self.region_profiles[self.current_region] # Set region-specific headers self.client.headers.update({ 'Accept-Language': self.profile['language'], 'X-User-Region': self.current_region, 'X-Currency': self.profile['currency'], 'X-Timezone': self.profile['timezone'] }) # Track latency measurements self.latency_measurements = [] print(f"User initialized for region: {self.current_region}") @task(3) def test_api_latency(self): """Test API response times and validate against regional expectations""" start_time = time.time() with self.client.get( "/api/v1/data", name=f"API Latency - {self.current_region}", catch_response=True ) as response: latency = (time.time() - start_time) * 1000 # Convert to ms self.latency_measurements.append(latency) if response.status_code == 200: # Validate latency against regional expectations expected_max = self.profile['expected_latency'] * 2 # Allow 2x baseline if latency <= expected_max: response.success() print(f"Good latency from {self.current_region}: {latency:.1f}ms") else: response.failure(f"High latency from {self.current_region}: {latency:.1f}ms (expected < {expected_max}ms)") else: response.failure(f"API request failed: {response.status_code}") @task(2) def test_cdn_performance(self): """Test CDN content delivery performance""" # Test different types of content content_types = [ {'path': '/static/images/hero.jpg', 'type': 'image'}, {'path': '/static/css/main.css', 'type': 'css'}, {'path': '/static/js/app.js', 'type': 'javascript'}, {'path': '/api/v1/config.json', 'type': 'json'} ] content = random.choice(content_types) start_time = time.time() with self.client.get( content['path'], name=f"CDN {content['type']} - {self.current_region}", catch_response=True ) as response: download_time = (time.time() - start_time) * 1000 if response.status_code == 200: # Check for CDN headers cdn_headers = ['X-Cache', 'CF-Cache-Status', 'X-Served-By', 'X-CDN'] cdn_hit = any(header in response.headers for header in cdn_headers) if cdn_hit: print(f"CDN hit for {content['type']} from {self.current_region}: {download_time:.1f}ms") response.success() else: print(f"CDN miss for {content['type']} from {self.current_region}: {download_time:.1f}ms") response.success() # Still successful, just noting CDN status else: response.failure(f"CDN content failed: {response.status_code}") @task(2) def test_regional_content(self): """Test region-specific content delivery""" regional_endpoints = [ f"/api/v1/content/regional?region={self.current_region}", f"/api/v1/pricing?currency={self.profile['currency']}", f"/api/v1/localization?lang={self.profile['language']}" ] endpoint = random.choice(regional_endpoints) with self.client.get( endpoint, name=f"Regional Content - {self.current_region}", catch_response=True ) as response: if response.status_code == 200: try: data = response.json() # Validate region-specific content if 'currency' in data and data['currency'] == self.profile['currency']: print(f"Correct currency for {self.current_region}: {data['currency']}") if 'language' in data and data['language'] == self.profile['language']: print(f"Correct language for {self.current_region}: {data['language']}") response.success() except json.JSONDecodeError: response.success() # Content might not be JSON else: response.failure(f"Regional content failed: {response.status_code}") @task(1) def test_regional_failover(self): """Test regional failover mechanisms""" # Simulate accessing primary and backup regional endpoints endpoints = [ f"/api/v1/health/region/{self.current_region}", f"/api/v1/status/regional" ] for endpoint in endpoints: with self.client.get( endpoint, name=f"Regional Failover - {self.current_region}", catch_response=True ) as response: if response.status_code == 200: try: health_data = response.json() if health_data.get('status') == 'healthy': print(f"Regional health check passed for {self.current_region}") response.success() break # Primary region is healthy else: print(f"Regional health degraded for {self.current_region}") continue # Try backup except json.JSONDecodeError: response.success() # Health check might return simple text break else: print(f"Regional endpoint failed: {response.status_code}") continue # Try next endpoint def on_stop(self): """Report latency statistics for this region""" if self.latency_measurements: avg_latency = sum(self.latency_measurements) / len(self.latency_measurements) min_latency = min(self.latency_measurements) max_latency = max(self.latency_measurements) print(f"Region {self.current_region} latency stats:") print(f" Average: {avg_latency:.1f}ms") print(f" Min: {min_latency:.1f}ms") print(f" Max: {max_latency:.1f}ms") print(f" Expected baseline: {self.profile['expected_latency']}ms") class RegionalLoadPatternUser(MultiRegionUser): """Simulate realistic regional load patterns based on business hours""" def on_start(self): super().on_start() # Adjust request frequency based on regional business hours current_hour = datetime.now().hour if current_hour in self.profile['peak_hours']: self.wait_time = between(0.5, 1.5) # Higher frequency during peak print(f"Peak hours for {self.current_region} - increased load") else: self.wait_time = between(2, 5) # Lower frequency during off-peak print(f"Off-peak hours for {self.current_region} - reduced load") @task(4) def simulate_business_hours_activity(self): """Simulate typical business hours activity patterns""" current_hour = datetime.now().hour if current_hour in self.profile['peak_hours']: # Peak hours - more intensive operations operations = [ '/api/v1/dashboard', '/api/v1/reports/generate', '/api/v1/transactions', '/api/v1/analytics' ] else: # Off-peak - lighter operations operations = [ '/api/v1/status', '/api/v1/notifications', '/api/v1/settings' ] operation = random.choice(operations) with self.client.get( operation, name=f"Business Hours Activity - {self.current_region}", catch_response=True ) as response: if response.status_code == 200: response.success() else: response.failure(f"Business hours operation failed: {response.status_code}") ``` ## Configuration Update these settings for your multi-region testing: ```python theme={null} # Regional Configuration REGIONS = { 'us_east': {'timezone': 'America/New_York', 'language': 'en-US', 'currency': 'USD'}, 'us_west': {'timezone': 'America/Los_Angeles', 'language': 'en-US', 'currency': 'USD'}, 'europe': {'timezone': 'Europe/London', 'language': 'en-GB', 'currency': 'EUR'}, 'asia_pacific': {'timezone': 'Asia/Singapore', 'language': 'en-SG', 'currency': 'SGD'} } # Latency Expectations (milliseconds) EXPECTED_LATENCY = { 'us_east': 50, 'us_west': 80, 'europe': 120, 'asia_pacific': 200 } ``` ## LoadForge Multi-Region Setup LoadForge allows you to select specific DigitalOcean regions for your load generators: ### Available Regions * **NYC1, NYC2, NYC3** - New York (US East) * **SFO1, SFO2, SFO3** - San Francisco (US West) * **LON1** - London (Europe) * **AMS2, AMS3** - Amsterdam (Europe) * **SGP1** - Singapore (Asia Pacific) * **TOR1** - Toronto (North America) * **FRA1** - Frankfurt (Europe) * **BLR1** - Bangalore (Asia) ### Testing Strategy 1. **Single Region Testing**: Test from one region to validate baseline performance 2. **Multi-Region Comparison**: Run identical tests from different regions simultaneously 3. **Regional Failover**: Test failover between primary and secondary regions 4. **Peak Hours Simulation**: Schedule tests during regional business hours ### Key Metrics to Monitor * **Response Time by Region**: Compare latency across different source regions * **CDN Hit Rates**: Validate content delivery network effectiveness * **Regional Failover Time**: Measure time to switch between regions * **Currency/Language Accuracy**: Validate localization correctness ## Best Practices * **Baseline Establishment**: Test from each region individually first * **Realistic Load Patterns**: Adjust test intensity based on regional business hours * **CDN Validation**: Verify content delivery network performance * **Failover Testing**: Test regional redundancy and disaster recovery ## Troubleshooting * **High Latency**: Check network routing and CDN configuration * **Regional Failures**: Validate failover mechanisms and backup regions * **Localization Problems**: Check currency, language, and timezone handling This locustfile provides comprehensive multi-region testing capabilities, leveraging LoadForge's DigitalOcean region selection to simulate realistic global user patterns and validate geographic performance consistency. # Regional Failover Testing Source: https://docs.loadforge.com/examples/geographic-localization/regional-failover-testing Test your application's regional failover and disaster recovery capabilities across different geographic locations # Regional Failover Testing Test your application's regional failover and disaster recovery capabilities across different geographic locations. This locustfile validates automatic failover, backup region activation, and service continuity during regional outages. ## Use Cases * **Disaster Recovery**: Test automatic failover during regional outages * **Load Balancing**: Validate traffic distribution across regions * **Health Monitoring**: Test regional health checks and monitoring * **Service Continuity**: Ensure uninterrupted service during failover * **Data Replication**: Validate cross-region data synchronization ## Locustfile ```python theme={null} from locust import HttpUser, task, between import random import time import json class RegionalFailoverUser(HttpUser): """Test regional failover and disaster recovery""" wait_time = between(2, 4) def on_start(self): """Initialize regional failover testing""" self.regions = { 'primary': { 'name': 'us-east-1', 'endpoint': '/api/v1', 'health_check': '/health', 'priority': 1 }, 'secondary': { 'name': 'us-west-2', 'endpoint': '/api/v2', 'health_check': '/health', 'priority': 2 }, 'tertiary': { 'name': 'eu-west-1', 'endpoint': '/api/v3', 'health_check': '/health', 'priority': 3 } } self.current_region = 'primary' self.failover_attempts = 0 self.max_failover_attempts = 3 print(f"Regional failover testing initialized - starting with {self.current_region}") @task(4) def test_primary_region_health(self): """Test primary region health and availability""" region_config = self.regions[self.current_region] health_endpoint = f"{region_config['endpoint']}{region_config['health_check']}" with self.client.get( health_endpoint, name=f"Health Check - {region_config['name']}", catch_response=True ) as response: if response.status_code == 200: try: health_data = response.json() if health_data.get('status') == 'healthy': print(f"Region {region_config['name']} is healthy") response.success() elif health_data.get('status') == 'degraded': print(f"Region {region_config['name']} is degraded - may trigger failover") response.success() self.consider_failover() else: print(f"Region {region_config['name']} is unhealthy") response.failure("Region unhealthy") self.trigger_failover() except json.JSONDecodeError: response.failure("Invalid health check response") self.trigger_failover() else: print(f"Health check failed for {region_config['name']}: {response.status_code}") response.failure(f"Health check failed: {response.status_code}") self.trigger_failover() @task(3) def test_api_functionality(self): """Test core API functionality in current region""" region_config = self.regions[self.current_region] api_endpoint = f"{region_config['endpoint']}/data" with self.client.get( api_endpoint, name=f"API Test - {region_config['name']}", catch_response=True ) as response: if response.status_code == 200: try: data = response.json() # Validate response includes region information if 'region' in data: response_region = data['region'] if response_region == region_config['name']: print(f"API response from correct region: {response_region}") response.success() else: print(f"API response from unexpected region: {response_region}") response.success() # Still functional, just different region else: response.success() except json.JSONDecodeError: response.failure("Invalid API response") else: print(f"API request failed in {region_config['name']}: {response.status_code}") response.failure(f"API failed: {response.status_code}") self.trigger_failover() @task(2) def test_data_consistency(self): """Test data consistency across regions""" region_config = self.regions[self.current_region] # Write data to current region test_data = { 'id': f"test_{int(time.time())}", 'region': region_config['name'], 'timestamp': time.time() } with self.client.post( f"{region_config['endpoint']}/data", json=test_data, name=f"Data Write - {region_config['name']}", catch_response=True ) as write_response: if write_response.status_code in [200, 201]: # Wait briefly for replication time.sleep(1) # Read data back to verify consistency with self.client.get( f"{region_config['endpoint']}/data/{test_data['id']}", name=f"Data Read - {region_config['name']}", catch_response=True ) as read_response: if read_response.status_code == 200: try: read_data = read_response.json() if read_data.get('id') == test_data['id']: print(f"Data consistency verified in {region_config['name']}") write_response.success() else: write_response.failure("Data consistency check failed") except json.JSONDecodeError: write_response.failure("Invalid data read response") else: write_response.failure("Data read failed after write") else: write_response.failure(f"Data write failed: {write_response.status_code}") @task(1) def test_cross_region_replication(self): """Test cross-region data replication""" if self.current_region == 'primary': # Test replication to secondary region secondary_config = self.regions['secondary'] with self.client.get( f"{secondary_config['endpoint']}/replication/status", name=f"Replication Status - {secondary_config['name']}", catch_response=True ) as response: if response.status_code == 200: try: replication_data = response.json() lag_seconds = replication_data.get('lag_seconds', 0) if lag_seconds < 60: # Less than 1 minute lag print(f"Replication lag acceptable: {lag_seconds}s") response.success() else: print(f"High replication lag: {lag_seconds}s") response.failure("High replication lag") except json.JSONDecodeError: response.failure("Invalid replication status response") else: response.failure(f"Replication status check failed: {response.status_code}") def consider_failover(self): """Consider failover based on degraded performance""" # Implement logic to decide if failover should be triggered # This could be based on response times, error rates, etc. if random.random() < 0.1: # 10% chance to trigger failover on degraded status self.trigger_failover() def trigger_failover(self): """Trigger failover to next available region""" if self.failover_attempts >= self.max_failover_attempts: print("Maximum failover attempts reached") return current_priority = self.regions[self.current_region]['priority'] # Find next available region next_region = None for region_key, region_config in self.regions.items(): if region_config['priority'] > current_priority: if next_region is None or region_config['priority'] < self.regions[next_region]['priority']: next_region = region_key if next_region: old_region = self.current_region self.current_region = next_region self.failover_attempts += 1 print(f"FAILOVER: Switching from {self.regions[old_region]['name']} to {self.regions[next_region]['name']}") # Test new region immediately self.test_failover_success() else: print("No available regions for failover") def test_failover_success(self): """Test that failover was successful""" region_config = self.regions[self.current_region] with self.client.get( f"{region_config['endpoint']}{region_config['health_check']}", name=f"Failover Verification - {region_config['name']}", catch_response=True ) as response: if response.status_code == 200: try: health_data = response.json() if health_data.get('status') == 'healthy': print(f"Failover successful - {region_config['name']} is healthy") else: print(f"Failover target {region_config['name']} is not healthy") self.trigger_failover() # Try next region except json.JSONDecodeError: print(f"Invalid health response from failover target {region_config['name']}") self.trigger_failover() else: print(f"Failover target {region_config['name']} is not responding") self.trigger_failover() class LoadBalancingUser(HttpUser): """Test load balancing across multiple regions""" wait_time = between(1, 2) def on_start(self): """Initialize load balancing testing""" self.regions = ['us-east-1', 'us-west-2', 'eu-west-1'] self.request_counts = {region: 0 for region in self.regions} @task(5) def test_load_distribution(self): """Test that load is distributed across regions""" with self.client.get( '/api/v1/load-balanced', name="Load Balanced Request", catch_response=True ) as response: if response.status_code == 200: try: data = response.json() serving_region = data.get('region', 'unknown') if serving_region in self.request_counts: self.request_counts[serving_region] += 1 print(f"Request served by {serving_region}") response.success() else: print(f"Request served by unknown region: {serving_region}") response.success() except json.JSONDecodeError: response.failure("Invalid load balanced response") else: response.failure(f"Load balanced request failed: {response.status_code}") def on_stop(self): """Report load distribution statistics""" total_requests = sum(self.request_counts.values()) if total_requests > 0: print("Load Distribution Summary:") for region, count in self.request_counts.items(): percentage = (count / total_requests) * 100 print(f" {region}: {count} requests ({percentage:.1f}%)") ``` ## Configuration Update these settings for your regional failover testing: ```python theme={null} # Regional Configuration REGIONS = { 'primary': {'name': 'us-east-1', 'priority': 1}, 'secondary': {'name': 'us-west-2', 'priority': 2}, 'tertiary': {'name': 'eu-west-1', 'priority': 3} } # Failover Settings MAX_FAILOVER_ATTEMPTS = 3 HEALTH_CHECK_INTERVAL = 30 # seconds REPLICATION_LAG_THRESHOLD = 60 # seconds ``` ## LoadForge Multi-Region Failover Testing ### Testing Strategy Use LoadForge's region selection to simulate realistic failover scenarios: 1. **Primary Region Testing**: Run from primary region (e.g., NYC) to test normal operations 2. **Failover Simulation**: Simulate primary region failure and test secondary activation 3. **Cross-Region Testing**: Test from multiple regions simultaneously to validate load balancing 4. **Recovery Testing**: Test failback to primary region after recovery ### Key Metrics to Monitor * **Failover Time**: Time taken to switch to backup region * **Service Availability**: Uptime during failover events * **Data Consistency**: Cross-region data synchronization * **Load Distribution**: Traffic distribution across healthy regions * **Recovery Time**: Time to restore primary region service ## Best Practices * **Health Check Frequency**: Regular monitoring of regional health * **Automated Failover**: Implement automatic failover triggers * **Data Replication**: Ensure real-time cross-region data sync * **Graceful Degradation**: Maintain partial functionality during outages * **Failback Planning**: Plan for returning to primary region ## Troubleshooting * **Slow Failover**: Check health check frequency and failover triggers * **Data Inconsistency**: Review replication lag and synchronization * **Load Imbalance**: Verify load balancing algorithms and region weights * **Failed Recovery**: Check primary region restoration procedures This locustfile provides comprehensive regional failover testing, ensuring your application maintains high availability across geographic regions and handles disaster recovery scenarios effectively. # Adding Custom Sleep Intervals Source: https://docs.loadforge.com/examples/getting-started/adding-sleeps Incorporate user-like delays in your load tests using custom sleep times. ## Overview While testing, it's crucial to simulate user behavior as realistically as possible. One key aspect of this simulation is introducing waits or pauses that a typical user might experience, like reading a webpage or waiting for some data. By default, LoadForge's `wait_time` attribute allows you to set such pauses. This guide will delve deeper into customizing these waits and understanding their applications. ## wait\_time vs. time.sleep There are two primary ways to introduce waits in your tests: 1. **`wait_time`**: This attribute specifies the pause between tasks. For example, once `load_page` concludes, the simulated user would wait for the duration specified by `wait_time` before initiating the next task. In our example below, after the `load_page` task concludes, a user will pause for a duration anywhere between 10 to 20 seconds. 2. **`time.sleep`**: This function allows for more specific pauses, right at the point it's called within your test. So if you want a user to pause immediately after a particular HTTP request and before another, `time.sleep` can be used. ## Example Code Here's an example: ```python theme={null} import random from locust import HttpUser, TaskSet, task, between import time class AwesomeUser(HttpUser): # User waits between 10 to 20 seconds after completing each task. wait_time = between(10, 20) @task(1) def load_page(self): self.client.get('/page1') # Here, the user waits between 10 to 60 seconds, # simulating a pause on page1 before navigating to page2. time.sleep(random.randint(10, 60)) self.client.get('/page2') # This simulates that a user pauses on page2 for exactly 20 seconds. time.sleep(20) ``` This test will simulate a user accessing `/page1`, waiting for a duration between 10 to 60 seconds (perhaps reading content or interacting with elements), and then navigating to `/page2`, where they pause again for a fixed 20 seconds. LoadForge leverages the power of the open-source `locust` library. This means that if you're already a locust user, you can directly port this script. If you wish to amplify your testing capabilities, consider importing your script into [LoadForge](https://loadforge.com)! # POSTing JSON data Source: https://docs.loadforge.com/examples/getting-started/json-posts Learn how to send an HTTP POST request with a JSON data payload using LoadForge. ## Overview Using LoadForge, sending HTTP POST requests with JSON data is straightforward. Before making the request, you need to prepare and parse your JSON data. This documentation will guide you on how to send a POST request with JSON data using LoadForge. *** ## Preparing JSON Data for POST Request To send JSON data via an HTTP POST request, the data needs to be serialized before transmission. In Python, you can utilize the `json.dumps()` method to serialize your data. In addition to preparing your data, you should also set the content type of your request to `application/json` in the headers to inform the receiving server that you're sending JSON data. ## Example: Posting JSON Data to an Endpoint In the provided example, a payload consisting of two key-value pairs will be posted to the `/post/endpoint`. Follow the steps and the code sample below: 1. **Setup and Imports**\ Start by importing necessary modules like `time`, `json`, and relevant `locust` classes. 2. **Define User Behavior**\ Create a class (`QuickstartUser`) that extends the `HttpUser` class from Locust. This will define the behavior of a simulated user. 3. **Set Waiting Time**\ Use `wait_time` to simulate the wait period between tasks. This example uses a randomized wait time of 3 to 5 seconds. 4. **Define the Task**\ Within the user class, define a task named `api_page`. This task sends a POST request with the payload. ```python theme={null} import time, json from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(3, 5) @task(1) def api_page(self): payload = { 'key': 'value', 'question': 'answer123' } headers = {'content-type': 'application/json'} response = self.client.post( "/post/endpoint", data=json.dumps(payload), headers=headers ) ``` LoadForge is built on top of locust.io, allowing you to leverage the full capabilities of the open-source Locust framework. If you're already using Locust, consider importing your scripts to [LoadForge](https://loadforge.com) to enhance and simplify your load testing process. By following the above instructions, you can effectively send POST requests with JSON data using LoadForge. Remember always to prepare your data and set the appropriate headers to ensure successful data transmission. # API Examples (REST) Source: https://docs.loadforge.com/examples/getting-started/rest-api Testing REST-based HTTP APIs with Bearer Token Authentication ## Overview Load testing a REST API is streamlined and intuitive with LoadForge. The examples below demonstrate testing capabilities using a sample REST API. LoadForge provides the flexibility to define custom headers as required, and the ability to send requests based on the data returned from your API. Ensure you disable API rate limits associated with your token to prevent an influx of failure responses during testing. Below are two illustrative examples: 1. **Basic API Example**: This showcases a straightforward set of GET requests to an API, accompanied by bearer authentication using a custom header. 2. **Advanced API Example**: A more intricate example involving both POST and GET requests and dynamic responses based on returned data. ## Basic API Example The following Python script demonstrates how to conduct simple GET requests to your API with bearer token authentication: ```python theme={null} from locust import HttpUser, task, between class QuickstartUser(HttpUser): # The simulated user will pause for a duration between 5 and 9 seconds before executing the tasks. wait_time = between(5, 9) def on_start(self): # Initializing the Authorization header with the bearer token. self.client.headers.update({'Authorization': 'Bearer TOKEN_HERE'}) @task(1) def index_page(self): # Making four GET requests every wait_time duration. self.client.get("/api/hosts") self.client.get("/api/tests") self.client.get("/api/result/1488") self.client.get("/api/result/1463") ``` ## Advanced API Example This test provides insights into load testing the LoadForge REST API. The script below contains comprehensive comments to elucidate each step: The test script comprises four main segments: 1. Fetch random hosts and tests. 2. During initialization, retrieve all results, save the result IDs, and establish the bearer token. 3. Acquire a random result from the previously fetched list. 4. Create a new host through the API and subsequently delete it to maintain a clean slate. This exemplifies the convenience of load testing, be it for a Laravel API or any RESTful API. The capacity to retrieve all results and then fetch random result IDs is particularly beneficial, given the inherent ability to parse responses within the test. ```python theme={null} import random from locust import HttpUser, task, between class AdvancedUser(HttpUser): wait_time = between(5, 9) def get_results(self): self.results = [] # Retrieve all results and store their unique IDs for subsequent tasks. response = self.client.get("/api/results") for result in response.json(): self.results.append(result['id']) def on_start(self): # Distribute start times among users. self.wait() # Initialize the Bearer authentication token. self.client.headers.update({'Authorization': 'Bearer TOKEN_HERE'}) # Fetch and store all potential results. self.get_results() @task(1) def read_hosts(self): self.client.get("/api/hosts") @task(2) def read_tests(self): self.client.get("/api/tests") @task(5) def read_results(self): # Fetch a random result based on the previously retrieved list. self.client.get("/api/result/" + str(random.choice(self.results))) @task(1) def crud_host(self): # Create a new Host and extract its details from the response. response = self.client.post("/api/hosts/create", {'url': 'www.test.com', 'port': '443', 'protocol': 'https'}) host = response.json() # Clean up by deleting the recently created host. self.client.post("/api/hosts/delete", {"host_id": host['host_id']}) ``` LoadForge is built upon the robust foundation of locust, allowing open-source locust users to readily utilize this script. If you're already a locust aficionado, think about importing your script to [LoadForge](https://loadforge.com) and elevate your testing endeavors! # Including Static Content Source: https://docs.loadforge.com/examples/getting-started/static-content Learn how to include static content in your LoadForge load tests to measure the capacity of your web servers alongside the dynamic content. ## Overview Modern web pages often serve a mix of dynamic and static content. While dynamic content, such as database-driven content, is the primary source of load, static content (like images, stylesheets, and JavaScript) can also influence server performance. While it's common for LoadForge tests to exclude these static assets, testing them can offer a comprehensive view of a site's performance. If you're seeking automation, LoadForge offers a Wizard with an option to include static content. When activated, the Wizard generates a test that encompasses your images, stylesheets, and JavaScript files. Check out how to [scrape the page](scrape-pages) for more. ## Example: Load Testing Static Content In the example provided, we illustrate a test that requests an index page followed by requests for a CSS file, a JS file, and an image. Notice the image request; it has a custom weight `@task(4)`. This weight ensures that the image is requested four times more frequently than other assets, simulating a scenario where a page might have multiple images compared to other static content. ### Test Script ```python theme={null} from locust import HttpUser, task, between class QuickstartUser(HttpUser): # Defines the wait time between tasks wait_time = between(5, 9) # This task fetches the index page and its associated JS and CSS files @task(1) def index_page(self): self.client.get("/") self.client.get("/app.js") self.client.get("/app.css") # This task emulates loading an image and is set to run 4 times as frequently @task(4) def image_selection(self): self.client.get("/images/logo.jpg") ``` *** Did you know? LoadForge is built upon the robust foundation of locust.io. This means if you're an existing locust user, you can seamlessly copy and use this script. Want to amplify your load testing capabilities? Import your script to [LoadForge](https://loadforge.com) and experience advanced testing features! By including static content in your tests, you ensure that your server's full capacity—across both dynamic and static content—is measured, helping you prepare for real-world scenarios. # Checking Status Codes Source: https://docs.loadforge.com/examples/getting-started/status-codes Test that checks for a 404 response or considers it a failure. ## Overview In various testing scenarios, validating the response status code is crucial to ensure the health, availability, and expected behavior of web services and applications. LoadForge provides a straightforward mechanism to set expected status codes and automatically consider responses as failures if they don't match the expected status. This feature proves invaluable when validating APIs, ensuring error handling is in place, and monitoring web service behaviors. ## Expecting Specific Status Codes By default, LoadForge will regard any HTTP response with a status code other than 2xx (e.g., 200 OK) as a failure. However, there are scenarios where you might want to specifically test for error responses. One common case is to verify that a certain endpoint returns a 404 Not Found error as expected. For instance, if you have an endpoint that should not be accessible and needs to return a 404 error, you can script your test to explicitly check for this. The following code snippet demonstrates testing for a 404 status code: ### Sample Code ```python theme={null} import time from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(3, 5) @task(1) def index_page(self): # Basic GET request self.client.get("/") # Checking for a specific status code (404 in this case) with self.client.get("/does_not_exist/", catch_response=True) as response: if response.status_code == 404: response.success() else: response.failure(f"Unexpected status code: {response.status_code}") ``` In this example, the test checks the `/does_not_exist/` endpoint. If the status code is 404, the test passes; otherwise, an error is reported in your LoadForge report. ## Supercharge Your Testing with LoadForge LoadForge leverages the power of the open-source tool, locust. If you're already a locust user, you can directly import your existing locust scripts into [LoadForge](https://loadforge.com) to take your testing to the next level with more robust features and expansive infrastructure. # CDN Performance Testing Source: https://docs.loadforge.com/examples/infrastructure-scaling/cdn-performance-testing Comprehensive CDN endpoint testing with edge caching validation and global performance metrics This guide demonstrates comprehensive CDN performance testing in your locustfile. CDN testing involves validating cache behavior, edge location performance, and content delivery optimization across global regions. ## Use Cases * Testing CDN cache hit/miss ratios * Validating edge location performance * Testing cache invalidation and purging * Measuring global content delivery performance * Testing CDN failover and redundancy * Validating cache headers and TTL behavior ## Complete CDN Performance Testing Locustfile ```python theme={null} import time import random import hashlib import json from datetime import datetime, timedelta from urllib.parse import urlparse, urljoin from locust import HttpUser, task, between from locust.exception import StopUser import logging import requests import base64 class CDNPerformanceUser(HttpUser): wait_time = between(1, 3) def on_start(self): """Initialize CDN performance testing""" self.cdn_config = { # CDN Provider Settings - Configure these for your setup 'primary_cdn': 'https://cdn.example.com', 'backup_cdn': 'https://backup-cdn.example.com', 'origin_server': 'https://origin.example.com', # Edge Locations to Test 'edge_locations': [ 'https://us-east-1.cdn.example.com', 'https://us-west-1.cdn.example.com', 'https://eu-west-1.cdn.example.com', 'https://ap-southeast-1.cdn.example.com', 'https://ap-northeast-1.cdn.example.com' ], # Content Types to Test 'content_types': { 'static_assets': ['/css/main.css', '/js/app.js', '/images/logo.png'], 'api_responses': ['/api/v1/products', '/api/v1/categories'], 'large_files': ['/downloads/app.zip', '/videos/demo.mp4'], 'dynamic_content': ['/api/v1/user/profile', '/api/v1/cart'] }, # Cache Settings 'cache_test_params': { 'cache_busting_enabled': True, 'test_cache_headers': True, 'validate_etags': True, 'test_compression': True }, # Performance Thresholds - Adjust these for your requirements 'performance_thresholds': { 'cache_hit_response_time': 200, # ms 'cache_miss_response_time': 1000, # ms 'edge_location_max_time': 500, # ms 'origin_fallback_max_time': 2000 # ms } } # Initialize metrics tracking self.cdn_metrics = { 'cache_hits': 0, 'cache_misses': 0, 'edge_performance': {}, 'compression_ratios': [], 'cache_headers_valid': 0, 'failover_events': 0 } # Generate unique test session ID self.test_session_id = hashlib.md5(f"{time.time()}_{random.randint(1000, 9999)}".encode()).hexdigest()[:8] # Setup logging self.logger = logging.getLogger(__name__) # Validate CDN endpoints self.validate_cdn_endpoints() def validate_cdn_endpoints(self): """Validate CDN endpoint connectivity""" for location in self.cdn_config['edge_locations']: try: response = requests.get(f"{location}/health", timeout=5) if response.status_code == 200: self.logger.info(f"CDN edge location validated: {location}") else: self.logger.warning(f"CDN edge location issue: {location} - {response.status_code}") except Exception as e: self.logger.error(f"CDN edge location failed: {location} - {e}") def detect_cache_status(self, response): """Detect cache hit/miss from response headers""" cache_headers = [ 'cf-cache-status', # Cloudflare 'x-cache', # AWS CloudFront, Fastly 'x-served-by', # Fastly 'x-cache-status', # Generic 'cache-control' # Standard ] for header in cache_headers: if header in response.headers: value = response.headers[header].lower() if any(hit_indicator in value for hit_indicator in ['hit', 'cached', 'fresh']): return 'hit' elif any(miss_indicator in value for miss_indicator in ['miss', 'expired', 'stale']): return 'miss' # Fallback: detect by response time (heuristic) response_time = response.elapsed.total_seconds() * 1000 return 'hit' if response_time < 100 else 'miss' def calculate_compression_ratio(self, response): """Calculate content compression ratio""" content_length = response.headers.get('content-length') content_encoding = response.headers.get('content-encoding', '').lower() if content_length and content_encoding in ['gzip', 'br', 'deflate']: compressed_size = int(content_length) # Estimate original size (rough approximation) if content_encoding == 'gzip': estimated_original = compressed_size * 3 elif content_encoding == 'br': estimated_original = compressed_size * 4 else: # deflate estimated_original = compressed_size * 2.5 compression_ratio = (1 - compressed_size / estimated_original) * 100 return max(0, min(100, compression_ratio)) # Clamp between 0-100% return 0 @task(3) def test_static_asset_caching(self): """Test static asset caching performance""" asset_path = random.choice(self.cdn_config['content_types']['static_assets']) # Test primary CDN with self.client.get( asset_path, name="CDN Static Asset", catch_response=True ) as response: if response.status_code == 200: cache_status = self.detect_cache_status(response) response_time = response.elapsed.total_seconds() * 1000 if cache_status == 'hit': self.cdn_metrics['cache_hits'] += 1 threshold = self.cdn_config['performance_thresholds']['cache_hit_response_time'] if response_time <= threshold: response.success() self.logger.info(f"Cache HIT: {asset_path} - {response_time:.0f}ms") else: response.failure(f"Cache hit too slow: {response_time:.0f}ms > {threshold}ms") else: self.cdn_metrics['cache_misses'] += 1 threshold = self.cdn_config['performance_thresholds']['cache_miss_response_time'] if response_time <= threshold: response.success() self.logger.info(f"Cache MISS: {asset_path} - {response_time:.0f}ms") else: response.failure(f"Cache miss too slow: {response_time:.0f}ms > {threshold}ms") # Test compression compression_ratio = self.calculate_compression_ratio(response) if compression_ratio > 0: self.cdn_metrics['compression_ratios'].append(compression_ratio) self.logger.info(f"Compression: {compression_ratio:.1f}%") else: response.failure(f"Static asset failed: {response.status_code}") @task(2) def test_edge_location_performance(self): """Test performance across different edge locations""" edge_location = random.choice(self.cdn_config['edge_locations']) test_path = '/api/v1/products' # Parse edge location for metrics location_name = urlparse(edge_location).netloc.split('.')[0] with self.client.get( test_path, headers={'Host': urlparse(edge_location).netloc}, name=f"Edge Location - {location_name}", catch_response=True ) as response: if response.status_code == 200: response_time = response.elapsed.total_seconds() * 1000 threshold = self.cdn_config['performance_thresholds']['edge_location_max_time'] # Store edge performance metrics if location_name not in self.cdn_metrics['edge_performance']: self.cdn_metrics['edge_performance'][location_name] = [] self.cdn_metrics['edge_performance'][location_name].append(response_time) if response_time <= threshold: response.success() self.logger.info(f"Edge {location_name}: {response_time:.0f}ms") else: response.failure(f"Edge location too slow: {response_time:.0f}ms > {threshold}ms") else: response.failure(f"Edge location failed: {response.status_code}") @task(2) def test_large_file_delivery(self): """Test large file delivery performance""" large_file = random.choice(self.cdn_config['content_types']['large_files']) start_time = time.time() with self.client.get( large_file, name="CDN Large File", catch_response=True ) as response: download_time = time.time() - start_time if response.status_code == 200: # Calculate throughput content_length = response.headers.get('content-length') if content_length: file_size_mb = int(content_length) / (1024 * 1024) throughput_mbps = file_size_mb / download_time if download_time > 0 else 0 self.logger.info(f"Large file: {file_size_mb:.1f}MB in {download_time:.1f}s ({throughput_mbps:.1f} MB/s)") # Validate reasonable download speed (>1 MB/s) if throughput_mbps >= 1.0: response.success() else: response.failure(f"Download too slow: {throughput_mbps:.1f} MB/s") else: # No content-length header, validate by time if download_time < 30: # 30 second timeout response.success() else: response.failure(f"Download timeout: {download_time:.1f}s") elif response.status_code == 206: # Partial content (byte-range) response.success() self.logger.info("Byte-range request successful") else: response.failure(f"Large file delivery failed: {response.status_code}") @task(1) def test_api_response_caching(self): """Test API response caching""" api_endpoint = random.choice(self.cdn_config['content_types']['api_responses']) with self.client.get( api_endpoint, name="CDN API Response", catch_response=True ) as response: if response.status_code == 200: cache_status = self.detect_cache_status(response) # Validate cache headers cache_control = response.headers.get('cache-control', '') etag = response.headers.get('etag', '') if cache_control or etag: self.cdn_metrics['cache_headers_valid'] += 1 self.logger.info(f"API cache headers valid: {api_endpoint}") response.success() else: response.failure(f"API response caching failed: {response.status_code}") @task(1) def test_cache_invalidation(self): """Test cache invalidation behavior""" test_path = '/api/v1/cache-test' cache_buster = f"?v={int(time.time())}" if self.cdn_config['cache_test_params']['cache_busting_enabled'] else "" with self.client.get( f"{test_path}{cache_buster}", name="Cache Invalidation Test", catch_response=True ) as response: if response.status_code == 200: cache_status = self.detect_cache_status(response) if cache_buster and cache_status == 'miss': response.success() self.logger.info("Cache invalidation working (cache miss with buster)") elif not cache_buster: response.success() self.logger.info(f"Cache behavior: {cache_status}") else: response.failure("Cache invalidation not working properly") else: response.failure(f"Cache invalidation test failed: {response.status_code}") @task(1) def test_cdn_failover(self): """Test CDN failover to backup or origin""" # Simulate primary CDN failure by testing backup backup_cdn = self.cdn_config['backup_cdn'] test_path = '/api/v1/products' with self.client.get( test_path, headers={'Host': urlparse(backup_cdn).netloc}, name="CDN Failover Test", catch_response=True ) as response: if response.status_code == 200: response_time = response.elapsed.total_seconds() * 1000 threshold = self.cdn_config['performance_thresholds']['origin_fallback_max_time'] self.cdn_metrics['failover_events'] += 1 if response_time <= threshold: response.success() self.logger.info(f"CDN failover successful: {response_time:.0f}ms") else: response.failure(f"CDN failover too slow: {response_time:.0f}ms > {threshold}ms") else: response.failure(f"CDN failover failed: {response.status_code}") @task(1) def test_image_optimization(self): """Test modern image format delivery (WebP, AVIF)""" image_path = '/images/hero.jpg' # Test WebP support with self.client.get( image_path, headers={'Accept': 'image/webp,image/*,*/*;q=0.8'}, name="Image Optimization - WebP", catch_response=True ) as response: if response.status_code == 200: content_type = response.headers.get('content-type', '').lower() if 'webp' in content_type: response.success() self.logger.info("WebP optimization active") elif 'jpeg' in content_type or 'jpg' in content_type: response.success() self.logger.info("Fallback to JPEG (WebP not supported)") else: response.failure(f"Unexpected image format: {content_type}") else: response.failure(f"Image optimization test failed: {response.status_code}") @task(1) def test_video_streaming(self): """Test video streaming with byte-range requests""" video_path = '/videos/demo.mp4' # Test byte-range request with self.client.get( video_path, headers={'Range': 'bytes=0-1023'}, # First 1KB name="Video Streaming - Byte Range", catch_response=True ) as response: if response.status_code == 206: # Partial Content content_range = response.headers.get('content-range', '') accept_ranges = response.headers.get('accept-ranges', '') if 'bytes' in accept_ranges.lower(): response.success() self.logger.info(f"Video streaming supported: {content_range}") else: response.failure("Byte-range requests not supported") elif response.status_code == 200: # Full content returned (acceptable for small ranges) response.success() self.logger.info("Video streaming: full content returned") else: response.failure(f"Video streaming failed: {response.status_code}") # Advanced CDN testing with multi-provider support class MultiProviderCDNUser(CDNPerformanceUser): """Advanced CDN testing across multiple providers""" def on_start(self): super().on_start() # Multi-provider configuration self.provider_configs = { 'cloudflare': { 'endpoint': 'https://cdn-cf.example.com', 'cache_header': 'cf-cache-status', 'edge_locations': ['us', 'eu', 'asia'] }, 'aws_cloudfront': { 'endpoint': 'https://d123456789.cloudfront.net', 'cache_header': 'x-cache', 'edge_locations': ['us-east-1', 'eu-west-1', 'ap-southeast-1'] }, 'fastly': { 'endpoint': 'https://cdn-fastly.example.com', 'cache_header': 'x-served-by', 'edge_locations': ['us-west', 'eu-central', 'asia-pacific'] } } self.current_provider = random.choice(list(self.provider_configs.keys())) self.logger.info(f"Testing CDN provider: {self.current_provider}") @task def test_provider_specific_features(self): """Test provider-specific CDN features""" provider_config = self.provider_configs[self.current_provider] with self.client.get( '/api/v1/products', headers={'Host': urlparse(provider_config['endpoint']).netloc}, name=f"Provider Test - {self.current_provider}", catch_response=True ) as response: if response.status_code == 200: # Check provider-specific cache header cache_header = provider_config['cache_header'] if cache_header in response.headers: cache_value = response.headers[cache_header] self.logger.info(f"{self.current_provider} cache status: {cache_value}") response.success() else: response.failure(f"Missing {self.current_provider} cache header") else: response.failure(f"{self.current_provider} test failed: {response.status_code}") ``` # Crawling Real HTTP User Source: https://docs.loadforge.com/examples/infrastructure-scaling/crawl-website Index a site and crawl all the pages with a random delay as a real user would. ## Overview This documentation page provides an example of how to simulate a real user's browsing behavior. It demonstrates a test that replicates user actions on the [Locust documentation site](https://docs.locust.io/en/latest/). The test identifies all links in the site's table of contents and then accesses these links in a randomized manner, mimicking a genuine user's experience. ## Key Features * **Real User Simulation**: This test accesses links with a random delay between 10 to 600 seconds, accurately simulating user load. * **Versatility**: A prime example for those wanting to recreate real user actions on different websites. * **Driven by Locust**: The power of locust.io backs LoadForge, an esteemed open-source load testing tool. LoadForge offers a comprehensive test generation wizard. This tool can automatically discover content on your domain and incorporate it into your test file, streamlining the process. ## Test Script Outlined below is the Python script underpinning this test: ```python theme={null} import random from locust import HttpUser, between, task from pyquery import PyQuery class AwesomeUser(HttpUser): # Simulated user behavior indicates a longer wait time (from 10 to 600 seconds) # between pages due to the considerable content present on each page. wait_time = between(10, 600) def on_start(self): # Initial delay ensures users don't access the site all at once. self.wait() # The starting point for all users is assumed to be the index page. self.index_page() self.urls_on_current_page = self.toc_urls @task(10) def index_page(self): r = self.client.get("/") pq = PyQuery(r.content) link_elements = pq(".toctree-wrapper a.internal") self.toc_urls = [l.attrib["href"] for l in link_elements] @task(50) def load_page(self): url = random.choice(self.toc_urls) r = self.client.get(url) pq = PyQuery(r.content) link_elements = pq("a.internal") self.urls_on_current_page = [l.attrib["href"] for l in link_elements] @task(30) def load_sub_page(self): url = random.choice(self.urls_on_current_page) r = self.client.get(url) ``` LoadForge operates on locust.io's foundation. Thus, if you're a current user of locust, this script is fully compatible. To amplify your testing strategies, consider importing your scripts into [LoadForge](https://loadforge.com). ## Conclusion LoadForge equips both novices and experts in load testing with an array of tools and examples, optimizing the efficacy of your tests. The real HTTP user crawling instance is a testament to the myriad ways you can gauge and hone the performance of your online platforms. # Custom HTTP Headers Source: https://docs.loadforge.com/examples/infrastructure-scaling/custom-headers How to set custom headers with your requests ## Overview You can easily set any headers you need by tweaking your get() or post() calls to include a header set. This is commonly used for authentication on an API (see [api examples](rest-api)) The examples below show two methods of setting headers. You can set any key-value headers you need with LoadForge. ## Code Below we set our POST requests `Content-Type` to `application/vnd.api+json` and the GET request to `application/json` within the same test. ```python theme={null} self.client.post( "/login", data=json.dumps(payload), headers={"Content-Type": "application/vnd.api+json"} ) self.client.get("/api/test", headers={"Content-Type": "application/json"}) ``` ## Global Headers You can also set headers globally by defining the on\_start() section as shown below. This will ensure ALL requests have the custom header added. ```python theme={null} def on_start(self): self.client.headers.update({'Authorization': 'Bearer xxx'}) ``` ```python theme={null} def on_start(self): self.client.headers.update({'Content-Type': 'application/json'}) ``` ## Custom Host Header Sometimes you need to set a custom host header, for example if you are testing against an IP address or a non-standard port. You can do this by setting the `Host` header: ```python theme={null} def on_start(self): self.client.headers.update({'Host': 'www.loadforge.com'}) ``` Or, on a single get/post request: ```python theme={null} self.client.get("/api/test", headers={"Host": "www.loadforge.com"}) ``` These are just partial examples from a test. You may use the wizard or another example template and copy paste these into your test. *** LoadForge is powered by locust, meaning open source locust users can copy this script as well. If you are a locust user, consider importing your script to [LoadForge](https://loadforge.com) to supercharge your testing! # Efficiently Scraping Page Resources Source: https://docs.loadforge.com/examples/infrastructure-scaling/scrape-pages A comprehensive guide on leveraging LoadForge for automatic fetching of web page resources like styles, javascript, images, etc., using the HttpUserWithResources plugin for Locust. ## Overview LoadForge provides a seamless integration with the `HttpUserWithResources` plugin for Locust, which facilitates the automatic retrieval of crucial web page resources. While LoadForge offers the automatic retrieval of resources, it's recommended to construct your test in a manner where resources of interest are fetched manually. This is essential because LoadForge won't execute JavaScript. Hence, for AJAX resources, you might want to manually specify them. Alternatively, you can upload a browser recording in the .har format to achieve the same result. LoadForge boasts a comprehensive wizard for test generation. This wizard can meticulously identify all the static content on your domain and incorporate it into your test file without any hassle! ## Crafting the Code The provided code snippet is designed to request the root of your website (i.e., "/"). Additionally, it will download any matches found for the regular expression: `.*[^(js|png|css|gif|woff)]$`. To put it simply, it fetches resources with file extensions: .js, .png, .css, .gif, or .woff. ```python theme={null} from locust_plugins.users import HttpUserWithResources from locust import task class TestUserWithResources(HttpUserWithResources): # The following default values can be modified as per requirements: # bundle_resource_stats=False # default_resource_filter=".*[^(js)]$" @task def include_resources_default(self): self.client.get("/cart") @task def include_resources_true(self): self.client.get("/", resource_filter=".*[^(js|png|css|gif|woff)]$") @task def include_resources_false(self): self.client.get("/dashboard", include_resources=False) ``` *** LoadForge operates on the robust foundation of Locust. This compatibility ensures that users of open-source Locust can effortlessly adopt this script into their setups. If you're already leveraging Locust, think about integrating your script with [LoadForge](https://loadforge.com) to elevate your testing experience! # Simple Serverless Testing Source: https://docs.loadforge.com/examples/infrastructure-scaling/simple-serverless-testing Basic serverless function testing for AWS Lambda, Vercel Functions, and Netlify Functions This guide shows how to test serverless functions and measure cold start performance. Perfect for testing serverless APIs and function-based architectures. ## Use Cases * Test serverless function response times * Measure cold start performance * Validate function scaling behavior * Test different serverless providers ## Simple Implementation ```python theme={null} from locust import task, HttpUser import time import random class ServerlessTestUser(HttpUser): def on_start(self): # Serverless function endpoints to test self.functions = { "aws_lambda": { "url": "https://your-api-id.execute-api.region.amazonaws.com/prod/function", "name": "AWS Lambda" }, "vercel": { "url": "https://your-app.vercel.app/api/function", "name": "Vercel Function" }, "netlify": { "url": "https://your-app.netlify.app/.netlify/functions/function", "name": "Netlify Function" } } # Test payloads self.test_payloads = [ {"message": "Hello World"}, {"data": {"user_id": 123, "action": "test"}}, {"query": "simple test query"}, {} # Empty payload ] @task(3) def test_aws_lambda(self): """Test AWS Lambda function""" if "aws_lambda" in self.functions: function = self.functions["aws_lambda"] payload = random.choice(self.test_payloads) start_time = time.time() with self.client.post( function["url"], json=payload, name="AWS Lambda" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: print(f"AWS Lambda: {response_time:.0f}ms") # Check for cold start indicators if response_time > 1000: # > 1 second might be cold start print(f"AWS Lambda: Possible cold start ({response_time:.0f}ms)") else: response.failure(f"AWS Lambda error: {response.status_code}") @task(3) def test_vercel_function(self): """Test Vercel Function""" if "vercel" in self.functions: function = self.functions["vercel"] payload = random.choice(self.test_payloads) start_time = time.time() with self.client.post( function["url"], json=payload, name="Vercel Function" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: print(f"Vercel Function: {response_time:.0f}ms") # Vercel functions typically have faster cold starts if response_time > 500: print(f"Vercel Function: Possible cold start ({response_time:.0f}ms)") else: response.failure(f"Vercel Function error: {response.status_code}") @task(2) def test_netlify_function(self): """Test Netlify Function""" if "netlify" in self.functions: function = self.functions["netlify"] payload = random.choice(self.test_payloads) start_time = time.time() with self.client.post( function["url"], json=payload, name="Netlify Function" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: print(f"Netlify Function: {response_time:.0f}ms") if response_time > 800: print(f"Netlify Function: Possible cold start ({response_time:.0f}ms)") else: response.failure(f"Netlify Function error: {response.status_code}") @task(2) def test_cold_start_behavior(self): """Test cold start behavior by waiting between requests""" function_key = random.choice(list(self.functions.keys())) function = self.functions[function_key] # Wait to increase chance of cold start wait_time = random.uniform(30, 120) # 30-120 seconds print(f"Waiting {wait_time:.0f}s to test cold start for {function['name']}") time.sleep(wait_time) start_time = time.time() with self.client.get( function["url"], name=f"Cold Start - {function['name']}" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: print(f"Cold start test {function['name']}: {response_time:.0f}ms") # Analyze cold start performance if response_time > 2000: print(f"Cold start {function['name']}: SLOW ({response_time:.0f}ms)") elif response_time > 1000: print(f"Cold start {function['name']}: MODERATE ({response_time:.0f}ms)") else: print(f"Cold start {function['name']}: FAST ({response_time:.0f}ms)") @task(1) def test_function_scaling(self): """Test how functions handle concurrent requests""" function_key = random.choice(list(self.functions.keys())) function = self.functions[function_key] # Make multiple rapid requests to test scaling concurrent_requests = 3 results = [] for i in range(concurrent_requests): start_time = time.time() with self.client.get( function["url"], name=f"Scaling Test - {function['name']}" ) as response: response_time = (time.time() - start_time) * 1000 results.append(response_time) if response.status_code != 200: response.failure(f"Scaling test failed: {response.status_code}") if results: avg_time = sum(results) / len(results) max_time = max(results) print(f"Scaling test {function['name']}: avg {avg_time:.0f}ms, max {max_time:.0f}ms") @task(1) def test_function_with_different_payloads(self): """Test functions with different payload sizes""" function_key = random.choice(list(self.functions.keys())) function = self.functions[function_key] # Test with different payload sizes payloads = [ {"size": "small", "data": "x" * 100}, {"size": "medium", "data": "x" * 1000}, {"size": "large", "data": "x" * 10000} ] for payload in payloads: start_time = time.time() with self.client.post( function["url"], json=payload, name=f"Payload Test - {function['name']}" ) as response: response_time = (time.time() - start_time) * 1000 if response.status_code == 200: print(f"Payload test {function['name']} ({payload['size']}): {response_time:.0f}ms") else: print(f"Payload test {function['name']} ({payload['size']}): ERROR {response.status_code}") @task(1) def test_function_error_handling(self): """Test how functions handle errors""" function_key = random.choice(list(self.functions.keys())) function = self.functions[function_key] # Test with invalid data to trigger errors error_payloads = [ {"invalid": "data", "trigger_error": True}, {"malformed": "json"}, None # No payload ] error_payload = random.choice(error_payloads) with self.client.post( function["url"], json=error_payload, name=f"Error Test - {function['name']}" ) as response: if response.status_code >= 400: print(f"Error test {function['name']}: Properly returned {response.status_code}") elif response.status_code == 200: print(f"Error test {function['name']}: Handled error gracefully") else: print(f"Error test {function['name']}: Unexpected response {response.status_code}") ``` ## Setup Instructions 1. **AWS Lambda**: Deploy a test function and get the API Gateway URL 2. **Vercel**: Deploy a function to Vercel and get the function URL 3. **Netlify**: Deploy a function to Netlify and get the function URL 4. Replace the placeholder URLs with your actual function endpoints ## What This Tests * **Response Times**: Basic function execution speed * **Cold Starts**: Performance when functions haven't run recently * **Scaling**: How functions handle concurrent requests * **Error Handling**: Function behavior with invalid inputs ## Cold Start Analysis Typical cold start times: * **AWS Lambda**: 100ms - 2000ms (depends on runtime and size) * **Vercel Functions**: 50ms - 500ms (generally faster) * **Netlify Functions**: 100ms - 800ms (moderate performance) ## Performance Tips * **Keep Functions Small**: Smaller functions have faster cold starts * **Use Appropriate Runtimes**: Node.js typically faster than Python * **Warm Functions**: Regular requests keep functions warm * **Monitor Memory Usage**: Right-size function memory allocation ## Common Issues * **Cold Start Delays**: First request after idle period is slow * **Timeout Errors**: Functions exceeding execution time limits * **Memory Limits**: Functions running out of allocated memory * **Concurrent Limits**: Too many simultaneous executions # Advanced E-Commerce Test Example Source: https://docs.loadforge.com/examples/modern-web-frameworks/ecommerce-site Authentication, adding to cart, browsing products, and going to checkout ## Overview This enhanced LoadForge locust script simulates a more comprehensive user journey on an e-commerce website. It includes advanced behaviors such as user authentication, browsing through a variety of products, adding items to the cart, viewing the cart, and proceeding to checkout. The script is designed to mimic realistic user actions and gather detailed performance metrics. ## Code The below code can be run directly on LoadForge to test at scale. ```python theme={null} from locust import HttpUser, task, between import random class EcommerceUser(HttpUser): wait_time = between(1, 5) product_ids = [101, 102, 103, 104, 105] # Example product IDs cart_id = random.randint(1000, 9999) def on_start(self): self.user_login() def user_login(self): # Implement login logic here response = self.client.post("/login", {"username": "user", "password": "password"}) if response.status_code != 200: print("Failed to log in") @task(1) def view_products(self): self.client.get("/products") @task(2) def view_product_details(self): product_id = random.choice(self.product_ids) self.client.get(f"/products/{product_id}", name="/products/:id") @task(3) def add_to_cart(self): product_id = random.choice(self.product_ids) self.client.post("/cart", {"product_id": product_id, "quantity": 1}) @task(4) def view_cart(self): self.client.get(f"/cart/{self.cart_id}") @task(5) def checkout(self): self.client.post("/checkout", {"cart_id": self.cart_id}) ``` LoadForge is powered by locust, meaning open source locust users can copy this script as well. If you are a locust user, consider importing your script to [LoadForge](https://loadforge.com) to supercharge your testing! # NextJS Load Test Source: https://docs.loadforge.com/examples/modern-web-frameworks/nextjs A premade example for how to load test your NextJS website and understand its performance. ## Overview In this guide, we'll walk through creating a load test for a Next.js website using Locust, and running it with LoadForge. This specific test can be used with any JavaScript-based site (next, nuxt, react, etc) as it automatically crawls URLs on the site and uses those in the load test. You can authenticate, maintain sessions, and much more with LoadForge. Browse the docs for examples on what you need, or use your LoadForge account's wizard or AI test generator to make the test for you. ## Prerequisites Ensure you have: * Very basic understanding of Python. * Have set up a [LoadForge](https://loadforge.com) account. * Have added and verified your Host for testing ## Using BS4 LoadForge instances come pre-provisioned with many helpful tools, including beautifulsoup4, which we use for parsing html content in this test. You can install it yourself locally if you are busy developing your test with the following command: ```bash theme={null} pip install locust beautifulsoup4 ``` ## Creating Your Test File The example below demonstrates a user that crawls a webpage, discovering and loading linked pages and static content (like images, CSS, JS files). You can paste this locustfile directly into LoadForge to test your site. ```python theme={null} from bs4 import BeautifulSoup from locust import HttpUser, task, between class WebsiteUser(HttpUser): wait_time = between(1, 2.5) def on_start(self): self.crawl("/") def crawl(self, path): response = self.client.get(path, name="[Page] "+path) if response.status_code == 200: soup = BeautifulSoup(response.text, "html.parser") # Discover linked pages links = [a["href"] for a in soup.find_all("a", href=True)] for link in links: self.crawl(link) # Load static content (img, css, js) resources = [(link["href"], "CSS") for link in soup.find_all("link", href=True)] resources += [(script["src"], "JS") for script in soup.find_all("script", src=True)] resources += [(img["src"], "IMG") for img in soup.find_all("img", src=True)] for resource, res_type in resources: self.client.get(resource, name=f"[{res_type}] {resource}") @task(1) def load_homepage(self): self.client.get("/") ``` ## Notes on the script * `on_start`: Initiates crawling the homepage once per simulated user. * `crawl`: A recursive method making GET requests to paths, discovering and loading other linked resources. * `@task(1)`: Simulates users continually loading the homepage. ## Conclusion This guide provided a basic setup for load testing a Next.js website, crawling, and discovering links/resources. Ensure to optimize and adjust your locustfile based on real-world user interactions, website structure, and testing objectives. *** LoadForge operates on the robust foundation of Locust. This compatibility ensures that users of open-source Locust can effortlessly adopt this script into their setups. If you're already leveraging Locust, think about integrating your script with [LoadForge](https://loadforge.com) to elevate your testing experience! # ReactJS Load Test Source: https://docs.loadforge.com/examples/modern-web-frameworks/reactjs An example guide on how to effectively load test your ReactJS application utilizing Locust and LoadForge. ## Overview In this guide, we shall explore the process of creating a load test for a React.js website using Locust, and executing it via LoadForge. This particular test can be applied to any JavaScript-based site (React, Next, Nuxt, etc) as it automatically crawls URLs on the site and uses those in the load test. LoadForge provides a wealth of additional features like authentication, session maintenance, and more. Navigate through the documentation for specifics on your testing needs, or use the LoadForge account's wizard or AI test generator to set up the test for you. ## Prerequisites Before starting, ensure you: * Possess a fundamental understanding of Python. * Have created a [LoadForge](https://loadforge.com) account. * Have added and verified your Host for testing. ## Utilizing BS4 LoadForge instances are outfitted with several useful tools, one of which is beautifulsoup4, utilized for parsing HTML content in this test. To install it locally for test development, use the following command: ```bash theme={null} pip install locust beautifulsoup4 ``` ## Crafting Your Test File The following example illustrates a user that crawls a webpage, identifies and loads linked pages and static content (like images, CSS, JS files). You can insert this locustfile directly into LoadForge to test your React.js website. ```python theme={null} from bs4 import BeautifulSoup from locust import HttpUser, task, between class WebsiteUser(HttpUser): wait_time = between(1, 2.5) def on_start(self): self.crawl("/") def crawl(self, path): response = self.client.get(path, name="[Page] "+path) if response.status_code == 200: soup = BeautifulSoup(response.text, "html.parser") # Discover linked pages links = [a["href"] for a in soup.find_all("a", href=True)] for link in links: self.crawl(link) # Load static content (img, css, js) resources = [(link["href"], "CSS") for link in soup.find_all("link", href=True)] resources += [(script["src"], "JS") for script in soup.find_all("script", src=True)] resources += [(img["src"], "IMG") for img in soup.find_all("img", src=True)] for resource, res_type in resources: self.client.get(resource, name=f"[{res_type}] {resource}") @task(1) def load_homepage(self): self.client.get("/") ``` ## Insights on the Script * `on_start`: Triggers the crawling of the homepage once per simulated user. * `crawl`: A recursive function performing GET requests to paths, discovering and loading other linked resources. * `@task(1)`: Imitates users perpetually loading the homepage. ## Conclusion This guide has laid out a fundamental setup for load testing a React.js website, performing crawling, and discovering links/resources. Be sure to refine and modify your locustfile based on actual user interactions, website structure, and testing goals. LoadForge builds upon the sturdy foundation offered by Locust, ensuring seamless adoption of scripts by users familiar with open-source Locust. If you're already using Locust, consider integrating your script with [LoadForge](https://loadforge.com) to enhance your testing capabilities! # Load Testing a Single-Page Application (SPA) Source: https://docs.loadforge.com/examples/modern-web-frameworks/spa-load-test Comprehensive guide on load testing your SPA with the LoadForge platform ## Overview Single-page applications (SPAs) have become a popular choice in modern web development. With the dynamic retrieval of data, SPAs offer a seamless user experience as there is no need for full page refreshes. This poses some unique challenges for load testing, but with LoadForge, the process is made simple and efficient. ## Understanding Single-Page Applications (SPA) A Single-Page Application (SPA) is a type of web application that serves a singular web page. Instead of loading new pages, SPAs dynamically rewrite the current page with new data fetched from the server. Key Characteristics of SPAs: 1. **No Page Refresh**: In SPAs, full-page refreshes are never needed. 2. **Dynamic Content**: All required HTML, JavaScript, and CSS are either loaded initially or added dynamically as necessary, usually based on user interactions. ## Steps to Load Test an SPA with LoadForge ### 1. User Simulation The goal of load testing is to mimic real-world users interacting with your site. In the context of an SPA: * New users will typically request the initial page and any essential JavaScript resources. * Subsequent interactions, like browsing content, logging in, or checking out, usually involve JSON API requests. ### 2. Capture the Requests These JSON API requests are vital for a realistic load test. To capture them: * Use your browser's Developer Tools (usually accessible with F12 or right-click -> Inspect). * Alternatively, use the [Record Your Browser](/docs/record-your-browser) feature on LoadForge. This handy feature allows you to record interactions with your SPA and then generates a test script for you. ### 3. Leverage the LoadForge Wizard LoadForge's wizard simplifies the load testing process: * It automatically fetches all page dependencies, ensuring your tests start from a "fully loaded" SPA state. * Once you have the generated or hand-written script, setting up your load test becomes straightforward. Specify the number of virtual users, their average wait time between requests, and the total test duration. With these configurations in place, you're all set to run your test on LoadForge! *** Did you know? LoadForge is powered by [locust.io](https://locust.io). This means if you're an existing locust user, you can seamlessly use your scripts with LoadForge. For an enhanced testing experience, consider importing your locust script to [LoadForge](https://loadforge.com). ## Example Locust Script for an SPA Load Test To aid in your understanding, here's an example locust script tailored for SPAs: ```python theme={null} from locust import HttpUser, task, between class SPATestUser(HttpUser): wait_time = between(5, 15) @task def load_spa_page(self): self.client.get("/path_to_your_SPA") @task def simulate_user_interaction(self): headers = {"Content-Type": "application/json"} data = { # Your JSON data mimicking user action, e.g. login details } self.client.post("/path_to_api_endpoint", headers=headers, json=data) # You can add more tasks to simulate different user actions. ``` Remember, the more accurately your script simulates real-world user behavior, the more valuable your load test results will be! # WordPress Load Test Example Source: https://docs.loadforge.com/examples/modern-web-frameworks/wordpress Learn how to effectively load test a WordPress blog or CMS with LoadForge. This guide will demonstrate simulating real users browsing your blog posts at random using LoadForge and locust.io ## Overview Performance is crucial for ensuring a good user experience, and slow load times can negatively affect your website's SEO and user retention. By load testing (or stress testing), you can identify bottlenecks, server issues, or inefficiencies in your website code or configuration. LoadForge offers an easy way to do this, and the best part? You can start with a free account! ## Key Features of this Load Test * **Automatic Crawling**: Once set up, the test will automatically crawl valid URLs from your WordPress blog or CMS. This mimics the behavior of real users who might hop from one blog post to another. * **Random Browsing**: The test doesn't just stick to one URL. It picks links at random, offering a more realistic simulation of user behavior. * **Adaptability**: While this guide uses the example of a WordPress blog, the principles can be applied to many types of websites and web applications. ## Code Example The provided code below is designed to: * Request the `/blog/` page of your WordPress site. * Extract all the URLs from the anchor tags present on that page (i.e., your latest blog posts). * Simulate users browsing those URLs at random. Moreover, it simulates users accessing certain static contents like images, styles, and scripts which are essential for proper page rendering. ```python theme={null} from locust import HttpUser, task, between from pyquery import PyQuery import random class QuickstartUser(HttpUser): wait_time = between(5, 9) def index_page(self): r = self.client.get("/blog/") pq = PyQuery(r.content) link_elements = pq(".blog-list-post h2 a") self.blog_urls = [] for l in link_elements: if "href" in l.attrib: self.blog_urls.append(l.attrib["href"]) def on_start(self): self.index_page() @task(1) def static_content(self): # Fetch images self.client.get("/img/logo/logo.png") self.client.get("/img/images/cta_img.png") self.client.get("/img/images/footer_fire.png") # Fetch styles self.client.get("/css/compiled.css?id=629e6d86d0748098936a") # Fetch scripts self.client.get("/js/compiled.js?id=1a927dbcfa8d96520959") @task(3) def blog_post(self): url = random.choice(self.blog_urls) r = self.client.get(url) @task(3) def home_page(self): self.client.get("/blog/") ``` *** Did you know? LoadForge is powered by the robust open-source tool, locust.io. If you're already familiar with locust and have scripts ready, it's easy to import them to [LoadForge](https://loadforge.com) and leverage its advanced testing capabilities. *** **Tip**: Make sure to adjust the static content URLs and the selector used to fetch blog URLs based on your site's structure and design. This ensures the test's accuracy and relevance to your specific WordPress setup. # LoadForge Examples Source: https://docs.loadforge.com/examples/overview Browse example Locust scripts for load testing various scenarios with LoadForge Welcome to the LoadForge examples directory! Here you'll find a comprehensive collection of ready-to-use Locust test scripts for various load testing scenarios. ## What You'll Find Here These examples demonstrate how to test different types of applications and APIs using LoadForge. Each example includes: * **Complete working code** ready to use with LoadForge * **Real-world use cases** for different testing scenarios * **Best practices** for load testing specific technologies * **Detailed explanations** of testing patterns All examples use Python and Locust, which are natively supported by LoadForge. You can copy any example and use it directly in your LoadForge tests. ## Browse by Category Basic examples for beginners - REST APIs, static content, and simple patterns GraphQL, gRPC, XML-RPC, and advanced API testing patterns OAuth, JWT, API keys, and secure authentication patterns Stress testing, spike testing, and performance validation Selenium-based browser automation and user flow testing WebSockets, SSE, polling, and real-time communication testing Shopping carts, checkout flows, and business process testing React, Vue, Angular, and modern JavaScript framework testing ## Quick Start 1. **Browse the categories** to find an example that matches your use case 2. **Copy the example code** to your LoadForge test 3. **Customize** the URLs, parameters, and settings for your application 4. **Run your test** on LoadForge to see results Need help choosing the right example? Check out our [Quick Start Guide](/quickstart) or contact support at [help@loadforge.com](mailto:help@loadforge.com) ## Get Started Create your free LoadForge account and paste any example script to start load testing in minutes. # Controlling Request Rates Source: https://docs.loadforge.com/examples/performance-testing/controlling-request-rates Wait times, iterations per second, and other ways to control request rates in LoadForge. ## Overview There are various ways to control the requests per second, iterations, and user load in LoadForge. Naturally, the primary way to control the request rate is with the number of Virtual Users you specify. This guide will focus on additional control beyond that. ## Standard wait\_time Typical LoadForge scripts include a `wait_time` parameter. Examples of this are below: ```python theme={null} from locust import HttpUser, task, between, constant class QuickstartUser(HttpUser): wait_time = between(5, 9) # or wait_time = constant(0.5) @task(3) def index_page(self): self.client.get("/") ``` `wait_time` is designed to simulate a real browser, where users wait a random amount of time between requests due to them consuming the content on the page. ## Zero wait For the maximum possible request rate (RPS) do not specify a wait time and the script will loop as fast as possible. You should also consider FastHttpUser if doing that, for a significant improvement to the request rate while sacrificing some of the complex test abilities. ## Constant Rate The `constant_total_ips` helper will ensure that the total number of requests per second is constant, regardless of the number of users. This is useful when you want to ensure a constant load on your server, regardless of the number of users. This will not work with the `wait_time` attribute, as it will override it. Also note, IPS is iterations per second, and means your full test script. ### Code Snippet ```python theme={null} from locust import HttpUser, task from locust_plugins import constant_total_ips class MyUser(HttpUser): wait_time = constant_total_ips(5) # the below task has two requests, meaning # our rps will be around 10 per second (due to 5 IPS above) @task def t(self): self.client.get("/") self.client.get("/contact-us") ``` # Enhancing Test Performance with FastHTTP Source: https://docs.loadforge.com/examples/performance-testing/fasthttp-performance Leverage the FastHttpUser class to optimize load test throughput on LoadForge. ## Introduction By default, LoadForge harnesses the capabilities of the `HttpUser` class, an interface built atop Python's renowned `requests` library. While this provides vast opportunities to tailor your test, it may sometimes offer more than you need. For those eyeing improved throughput, LoadForge introduces `FastHttpUser`—a class that promises to amplify your test capacity by 4-5x, facilitating a greater number of requests per second compared to the default `HttpUser`. Please proceed cautiously: `FastHttpUser` is an advanced feature. While it offers speed, it sacrifices some of the functionalities inherent to the `HttpUser` class. It may not suit every user or every use case. ## Sample Test with FastHttpUser Here's a succinct test showcasing the usage of `FastHttpUser`. This test has no `wait_time` and is designed to deliver the maximum number of requests for each virtual user. ```python theme={null} from locust import task, FastHttpUser class MyUser(FastHttpUser): @task(1) def index(self): response = self.client.get("/") ``` *** ## Advanced Test with FastHttpUser Suppose you have an e-commerce site and want to test the performance of various pages: the homepage, product listings, and individual product pages. The following test simulates these scenarios using `FastHttpUser`: ### Code ```python theme={null} from locust import task, FastHttpUser, between import random class EcommerceUser(FastHttpUser): wait_time = between(1, 5) # simulates user thinking time between 1 to 5 seconds @task(3) # 3 times more likely to visit the homepage than other pages def visit_home(self): self.client.get("/") @task(2) def browse_products(self): self.client.get("/products") @task(1) def view_single_product(self): # this function would return a random product ID product_id = self.random_product_id() self.client.get(f"/products/{product_id}") def random_product_id(self): # assuming there are 1000 products return random.randint(1, 1000) ``` ### Weights & Endpoints In this advanced example, you'll notice the `@task` decorators are given different weights. These weights dictate the likelihood of a particular endpoint being hit. A higher weight means the task will be executed more frequently. This simulates real-world scenarios where users are more likely to visit some pages (like the homepage) than others. *** Remember, while `FastHttpUser` accelerates the request throughput, you might miss out on some of the functionalities offered by the standard `HttpUser`. Always make sure to weigh the pros and cons based on the specific requirements of your testing scenario. *** LoadForge is built on the sturdy foundation of `locust.io`, an open-source load testing framework. If you're already a user of `locust.io`, this script will look familiar. Interested in escalating your testing game? Import your `locust.io` script into [LoadForge](https://loadforge.com) and tap into a world of enhanced testing! # Checking Response Text and Times Source: https://docs.loadforge.com/examples/performance-testing/limiting-response-time Learn how to validate response content and ensure timely responses using LoadForge. ## Overview In typical scenarios, LoadForge would treat any valid HTTP response code as a success. However, there might be situations where you need to ensure that the response not only returns a valid HTTP code but also contains a specific string or conforms to a certain response time. In this guide, we will illustrate how to ensure a response contains the word "Success" and also demonstrate how to flag a test as failed if the response time exceeds 0.5 seconds. Monitoring and validating actual response times and content can be crucial to ensure that your web application or service meets performance criteria. ## Verifying Response Content and Timings Here is an example that demonstrates how to monitor the actual timings around a response. Using LoadForge, you can inspect the response content, review headers, monitor timings, and more. This code snippet would report a failure in your LoadForge test if the response does not meet the set criteria. ### Code Snippet ```python theme={null} import time from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(3, 5) @task(1) def index_page(self): with self.client.get("/", catch_response=True) as response: if "Success" not in response.text: response.failure("Expected response content not found") elif response.elapsed.total_seconds() > 0.5: response.failure("Response took longer than expected") ``` ## Setting Request Timeouts Another approach to ensure your tests are accurate and realistic is to set timeouts on your client requests. This ensures that if a response takes longer than the specified timeout, it's flagged as a failure. ### Example with Timeouts ```python theme={null} from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(5, 9) @task(1) def index_page(self): self.client.get('/page1', timeout=10) # A max of 10 seconds for this request self.client.get('/page2', timeout=5) # A max of 5 seconds for this request ``` *** LoadForge harnesses the power of locust, an open-source performance testing tool. This means locust users can readily leverage the above scripts for their own needs. If you're already using locust, consider migrating your script to [LoadForge](https://loadforge.com) to enhance your testing capabilities and experience! # Setting a Random User-Agent with LoadForge Source: https://docs.loadforge.com/examples/performance-testing/random-user-agents Learn how to create a dynamic list of user agents and set up each LoadForge client to randomly select one. ## Overview Customizing headers to fit specific needs is simple with LoadForge. This guide demonstrates how to set up a list of user agents and let each client choose one randomly during the test. This method provides a realistic approach to load testing, simulating different user agents accessing your application. Although our focus in this example is on user agents, the same principle can be applied to other headers, or even for randomly selecting users, paths, and more. ## Creating and Setting Random User Agents The example below demonstrates how to: 1. Define a list of user agents. 2. Use the `on_start` method to let each client pick a user agent randomly at the beginning of a test. 3. Maintain the selected user agent for each client throughout the test duration. ```python theme={null} import random from locust import HttpUser, task, between USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", ] class QuickstartUser(HttpUser): wait_time = between(3, 5) def on_start(self): selected_agent = random.choice(USER_AGENTS) self.headers = {"User-Agent": selected_agent} self.client.headers = self.headers @task(1) def load_page(self): self.client.get("/example-url") ``` Did you know? LoadForge is built on the powerful locust.io framework. This means if you're a locust user, you can directly import your scripts into [LoadForge](https://loadforge.com) to maximize your testing capabilities. Elevate your testing experience with LoadForge! # Accessibility Testing Source: https://docs.loadforge.com/examples/qa-testing/accessibility-testing Check WCAG compliance and accessibility issues across your website This guide demonstrates how to test website accessibility with LoadForge, checking for WCAG compliance issues and common accessibility problems. ## Use Cases * Detecting missing alt text and accessibility labels * Validating form labels and required field indicators * Checking heading structure and hierarchy * Finding generic or missing link text * Testing basic ARIA implementation ## Simple Accessibility Checker ```python theme={null} from locust import HttpUser, task, between import re import time class SimpleAccessibilityChecker(HttpUser): wait_time = between(1, 2) def on_start(self): """Initialize simple accessibility checking""" self.accessibility_issues = [] self.pages_checked = 0 self.discovered_pages = [] self.current_page_issues = [] print("Starting simple accessibility check...") @task(5) def discover_and_check_accessibility(self): """Discover pages and check accessibility continuously""" if self.pages_checked == 0: # Start with homepage self._check_page_accessibility('/') elif len(self.discovered_pages) > 0: # Keep cycling through discovered pages for continuous testing self._cycle_through_pages() def _check_page_accessibility(self, page_url): """Check accessibility on a single page""" self.current_page_issues = [] # Reset issues for this page with self.client.get(page_url, name=f"ACCESSIBILITY: {page_url}", catch_response=True) as response: if response.status_code == 200: self._check_basic_accessibility(page_url, response.text) # Only increment pages_checked during initial discovery phase if page_url == '/' and self.pages_checked == 0: self.pages_checked += 1 self._find_internal_pages(response.text) elif page_url in self.discovered_pages and self.pages_checked == 1: # We're in the continuous testing phase, don't increment counter pass else: # Still in discovery mode self.pages_checked += 1 # Report page result based on issues found if self.current_page_issues: high_issues = [i for i in self.current_page_issues if i['severity'] == 'HIGH'] medium_issues = [i for i in self.current_page_issues if i['severity'] == 'MEDIUM'] failure_msg = f"❌ {len(self.current_page_issues)} accessibility issues ({len(high_issues)} high, {len(medium_issues)} medium)" response.failure(failure_msg) else: response.success() else: response.failure(f"Could not access page: HTTP {response.status_code}") def _find_internal_pages(self, html_content): """Find internal pages from homepage links""" # Find internal links links = re.findall(r']+href=["\']([^"\']+)["\']', html_content, re.IGNORECASE) for link in links: if self._is_internal_page_link(link): normalized_link = self._normalize_link(link) if normalized_link and normalized_link not in self.discovered_pages and normalized_link != '/': self.discovered_pages.append(normalized_link) if len(self.discovered_pages) >= 100: # Discover up to 100 pages break print(f"Discovered {len(self.discovered_pages)} internal pages to check") def _cycle_through_pages(self): """Continuously cycle through discovered pages for accessibility testing""" if not self.discovered_pages: return # Pick a random page from discovered pages to check import random page_to_check = random.choice(self.discovered_pages) # Check accessibility on this page (this creates continuous load testing) self._check_page_accessibility(page_to_check) def _is_internal_page_link(self, link): """Check if link is an internal page (not resource)""" # Skip anchors, external protocols, and resources if any(skip in link.lower() for skip in ['#', 'mailto:', 'tel:', 'javascript:']): return False # Skip common resource extensions resource_extensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.pdf', '.zip'] if any(link.lower().endswith(ext) for ext in resource_extensions): return False # Must be internal (relative or same domain) if link.startswith('/') or not link.startswith('http'): return True return False def _normalize_link(self, link): """Normalize link for checking""" try: if link.startswith('/'): return link.split('#')[0] # Remove fragment elif not link.startswith('http'): return '/' + link.lstrip('./') return None except: return None def _check_basic_accessibility(self, page_url, html_content): """Check basic accessibility issues on a page""" print(f"Checking accessibility for: {page_url}") # Check images for alt text self._check_image_alt_text(page_url, html_content) # Check form labels self._check_form_labels(page_url, html_content) # Check heading structure self._check_heading_structure(page_url, html_content) # Check link text self._check_link_text(page_url, html_content) def _check_image_alt_text(self, page_url, html_content): """Check images for alt text""" img_tags = re.findall(r']*>', html_content, re.IGNORECASE) for img_tag in img_tags: if 'alt=' not in img_tag.lower(): self._log_issue(page_url, 'HIGH', 'Image missing alt attribute', img_tag[:80]) else: # Check for generic alt text alt_match = re.search(r'alt=["\']([^"\']*)["\']', img_tag, re.IGNORECASE) if alt_match: alt_text = alt_match.group(1).strip() if alt_text.lower() in ['image', 'picture', 'photo', 'img']: self._log_issue(page_url, 'MEDIUM', f'Generic alt text: "{alt_text}"', img_tag[:80]) def _check_form_labels(self, page_url, html_content): """Check form elements for labels""" form_elements = re.findall(r'<(input|textarea|select)[^>]*>', html_content, re.IGNORECASE) for element in form_elements: # Skip hidden inputs and buttons if 'type="hidden"' in element.lower() or 'type="submit"' in element.lower(): continue # Check for id and corresponding label id_match = re.search(r'id=["\']([^"\']*)["\']', element, re.IGNORECASE) if id_match: element_id = id_match.group(1) label_pattern = f']*for=["\']?{re.escape(element_id)}["\']?[^>]*>' if not re.search(label_pattern, html_content, re.IGNORECASE): # Check for aria-label as alternative if 'aria-label=' not in element.lower(): self._log_issue(page_url, 'HIGH', f'Form element missing label: {element_id}', element[:80]) def _check_heading_structure(self, page_url, html_content): """Check heading structure""" headings = re.findall(r'<(h[1-6])[^>]*>(.*?)', html_content, re.IGNORECASE | re.DOTALL) if not headings: self._log_issue(page_url, 'HIGH', 'Page has no headings', '') return # Check if page starts with h1 heading_levels = [int(h[0][1]) for h in headings] if heading_levels[0] != 1: self._log_issue(page_url, 'MEDIUM', f'Page does not start with h1 (starts with h{heading_levels[0]})', '') # Check for empty headings for heading_tag, heading_text in headings: clean_text = re.sub(r'<[^>]+>', '', heading_text).strip() if not clean_text: self._log_issue(page_url, 'HIGH', f'Empty {heading_tag.upper()} heading', '') def _check_link_text(self, page_url, html_content): """Check link text for accessibility""" links = re.finditer(r']*>(.*?)', html_content, re.IGNORECASE | re.DOTALL) for link_match in links: link_text = link_match.group(1).strip() clean_link_text = re.sub(r'<[^>]+>', '', link_text).strip() # Check for empty link text if not clean_link_text: self._log_issue(page_url, 'HIGH', 'Link has no text', link_match.group(0)[:80]) elif clean_link_text.lower() in ['click here', 'read more', 'more', 'here', 'link']: self._log_issue(page_url, 'MEDIUM', f'Generic link text: "{clean_link_text}"', link_match.group(0)[:80]) def _log_issue(self, page_url, severity, description, element): """Log accessibility issue""" issue = { 'page': page_url, 'severity': severity, 'description': description, 'element': element, 'timestamp': time.time() } self.accessibility_issues.append(issue) self.current_page_issues.append(issue) # Track issues for current page print(f"ACCESSIBILITY ISSUE [{severity}]: {description} on {page_url}") @task(1) def report_accessibility_status(self): """Report current accessibility status""" if len(self.discovered_pages) == 0: return high_issues = [issue for issue in self.accessibility_issues if issue['severity'] == 'HIGH'] medium_issues = [issue for issue in self.accessibility_issues if issue['severity'] == 'MEDIUM'] total_pages_in_scope = len(self.discovered_pages) + 1 # +1 for homepage print(f"ACCESSIBILITY LOAD TEST: {len(self.accessibility_issues)} total issues " f"({len(high_issues)} high, {len(medium_issues)} medium) " f"found across {total_pages_in_scope} pages (continuously testing)") def on_stop(self): """Final accessibility report""" print("\n" + "="*50) print("ACCESSIBILITY LOAD TEST COMPLETE") print("="*50) total_pages_in_scope = len(self.discovered_pages) + 1 # +1 for homepage print(f"Pages in test scope: {total_pages_in_scope}") print(f"Total accessibility issues found: {len(self.accessibility_issues)}") if self.accessibility_issues: print(f"\nTOP ACCESSIBILITY ISSUES:") for issue in self.accessibility_issues[:5]: print(f"❌ [{issue['severity']}] {issue['description']}") print(f" Page: {issue['page']}") else: print("✅ No accessibility issues found!") ``` ## Comprehensive Accessibility Testing ```python theme={null} from locust import HttpUser, task, between import json import time import re from urllib.parse import urlparse from collections import defaultdict, deque class ComprehensiveAccessibilityTester(HttpUser): wait_time = between(1, 3) def on_start(self): """Initialize comprehensive accessibility testing""" self.visited_pages = set() self.pages_to_check = deque(['/']) self.accessibility_issues = [] self.accessibility_stats = defaultdict(int) self.wcag_violations = [] self.base_domain = None # Initialize crawler self._initialize_crawler() def _initialize_crawler(self): """Initialize the accessibility crawler""" response = self.client.get('/', name="INIT: Homepage accessibility check") if response.status_code == 200: self.base_domain = urlparse(self.client.base_url).netloc print(f"Starting comprehensive accessibility testing for: {self.base_domain}") else: print(f"Failed to access homepage: {response.status_code}") @task(5) def crawl_and_test_accessibility(self): """Main crawling task to test accessibility""" if not self.pages_to_check: return current_page = self.pages_to_check.popleft() if current_page in self.visited_pages: return self.visited_pages.add(current_page) response = self.client.get(current_page, name=f"CRAWL: {current_page}") if response.status_code == 200: self._test_page_accessibility(current_page, response.text) self._find_more_pages(response.text) else: print(f"Cannot access page for accessibility testing: {current_page}") def _test_page_accessibility(self, page_url, html_content): """Test comprehensive accessibility on a page""" self.accessibility_stats['total_pages'] += 1 # Run all accessibility tests self._test_images(page_url, html_content) self._test_forms(page_url, html_content) self._test_headings(page_url, html_content) self._test_links(page_url, html_content) self._test_aria(page_url, html_content) self._test_semantic_html(page_url, html_content) def _test_images(self, page_url, html_content): """Test image accessibility""" img_tags = re.findall(r']*>', html_content, re.IGNORECASE) for img_tag in img_tags: self.accessibility_stats['total_images'] += 1 if 'alt=' not in img_tag.lower(): self._record_issue(page_url, 'HIGH', 'Image missing alt attribute', 'WCAG 1.1.1', img_tag[:100]) else: alt_match = re.search(r'alt=["\']([^"\']*)["\']', img_tag, re.IGNORECASE) if alt_match: alt_text = alt_match.group(1).strip() if not alt_text and 'role="presentation"' not in img_tag.lower(): self._record_issue(page_url, 'MEDIUM', 'Image has empty alt text', 'WCAG 1.1.1', img_tag[:100]) elif alt_text.lower() in ['image', 'picture', 'photo', 'img']: self._record_issue(page_url, 'MEDIUM', f'Generic alt text: "{alt_text}"', 'WCAG 1.1.1', img_tag[:100]) def _test_forms(self, page_url, html_content): """Test form accessibility""" form_elements = re.findall(r'<(input|textarea|select)[^>]*>', html_content, re.IGNORECASE) for element in form_elements: self.accessibility_stats['total_form_elements'] += 1 # Skip certain input types if any(t in element.lower() for t in ['type="hidden"', 'type="submit"', 'type="button"']): continue id_match = re.search(r'id=["\']([^"\']*)["\']', element, re.IGNORECASE) if id_match: element_id = id_match.group(1) label_pattern = f']*for=["\']?{re.escape(element_id)}["\']?[^>]*>' if not re.search(label_pattern, html_content, re.IGNORECASE): if 'aria-label=' not in element.lower() and 'aria-labelledby=' not in element.lower(): self._record_issue(page_url, 'HIGH', f'Form element missing label: {element_id}', 'WCAG 1.3.1', element[:100]) # Check required fields if 'required' in element.lower() and 'aria-required=' not in element.lower(): self._record_issue(page_url, 'MEDIUM', 'Required field missing aria-required', 'WCAG 3.3.2', element[:100]) def _test_headings(self, page_url, html_content): """Test heading structure""" headings = re.findall(r'<(h[1-6])[^>]*>(.*?)', html_content, re.IGNORECASE | re.DOTALL) if not headings: self._record_issue(page_url, 'HIGH', 'Page has no heading structure', 'WCAG 1.3.1', '') return heading_levels = [int(h[0][1]) for h in headings] # Check if starts with h1 if heading_levels[0] != 1: self._record_issue(page_url, 'MEDIUM', f'Page does not start with h1', 'WCAG 1.3.1', '') # Check for heading level skips for i in range(1, len(heading_levels)): if heading_levels[i] > heading_levels[i-1] + 1: self._record_issue(page_url, 'MEDIUM', f'Heading level skip: h{heading_levels[i-1]} to h{heading_levels[i]}', 'WCAG 1.3.1', '') # Check for empty headings for heading_tag, heading_text in headings: clean_text = re.sub(r'<[^>]+>', '', heading_text).strip() if not clean_text: self._record_issue(page_url, 'HIGH', f'Empty {heading_tag.upper()} heading', 'WCAG 2.4.6', '') def _test_links(self, page_url, html_content): """Test link accessibility""" for link_match in re.finditer(r']*>(.*?)', html_content, re.IGNORECASE | re.DOTALL): link_tag = link_match.group(0) link_text = link_match.group(1).strip() clean_link_text = re.sub(r'<[^>]+>', '', link_text).strip() self.accessibility_stats['total_links'] += 1 if not clean_link_text: if 'aria-label=' not in link_tag.lower() and 'title=' not in link_tag.lower(): self._record_issue(page_url, 'HIGH', 'Link has no accessible text', 'WCAG 2.4.4', link_tag[:100]) elif clean_link_text.lower() in ['click here', 'read more', 'more', 'link', 'here']: self._record_issue(page_url, 'MEDIUM', f'Generic link text: "{clean_link_text}"', 'WCAG 2.4.4', link_tag[:100]) def _test_aria(self, page_url, html_content): """Test ARIA implementation""" # Check for main landmark if 'role="main"' not in html_content.lower() and ' 20: # Limit crawling return links = re.findall(r']+href=["\']([^"\']+)["\']', html_content, re.IGNORECASE) for link in links: if self._is_internal_link(link) and link not in self.visited_pages: normalized_link = self._normalize_url(link) if normalized_link: self.pages_to_check.append(normalized_link) def _is_internal_link(self, link): """Check if link is internal""" if link.startswith('/'): return True if link.startswith('http'): return urlparse(link).netloc == self.base_domain return not link.startswith(('mailto:', 'tel:', 'javascript:', '#')) def _normalize_url(self, link): """Normalize URL for consistency""" try: if link.startswith('/'): return link.split('#')[0] # Remove fragments elif link.startswith('http'): parsed = urlparse(link) if parsed.netloc == self.base_domain: return parsed.path return None except: return None def _record_issue(self, page_url, severity, description, wcag_guideline, element): """Record accessibility issue""" issue = { 'page': page_url, 'severity': severity, 'description': description, 'wcag_guideline': wcag_guideline, 'element': element[:200] if element else '', 'timestamp': time.time() } self.accessibility_issues.append(issue) self.accessibility_stats['total_issues'] += 1 if wcag_guideline: self.wcag_violations.append(wcag_guideline) print(f"ACCESSIBILITY ISSUE [{severity}]: {description} ({wcag_guideline}) on {page_url}") @task(1) def generate_accessibility_report(self): """Generate accessibility report""" if len(self.visited_pages) < 3: return high_issues = [i for i in self.accessibility_issues if i['severity'] == 'HIGH'] medium_issues = [i for i in self.accessibility_issues if i['severity'] == 'MEDIUM'] print(f"ACCESSIBILITY REPORT: {len(self.accessibility_issues)} total issues " f"({len(high_issues)} high, {len(medium_issues)} medium) " f"across {len(self.visited_pages)} pages") def on_stop(self): """Final accessibility report""" print("\n=== COMPREHENSIVE ACCESSIBILITY TEST COMPLETE ===") print(f"Pages tested: {len(self.visited_pages)}") print(f"Total accessibility issues: {len(self.accessibility_issues)}") print(f"WCAG violations: {len(set(self.wcag_violations))}") if self.accessibility_issues: print("\nTOP ACCESSIBILITY ISSUES:") for issue in self.accessibility_issues[:5]: print(f" - [{issue['severity']}] {issue['description']} ({issue['wcag_guideline']})") ``` ## Advanced WCAG Compliance Testing ```python theme={null} from locust import HttpUser, task, between import json import time import re class AdvancedWCAGTester(HttpUser): wait_time = between(2, 4) @task(3) def test_wcag_aa_compliance(self): """Test specific WCAG AA compliance requirements""" pages = ['/', '/about', '/contact', '/services'] for page in pages: response = self.client.get(page, name=f"WCAG AA: {page}") if response.status_code == 200: self._test_color_contrast_requirements(page, response.text) self._test_focus_management(page, response.text) self._test_keyboard_navigation(page, response.text) def _test_color_contrast_requirements(self, page_url, html_content): """Test color contrast indicators""" # Look for potential low contrast in inline styles styles = re.findall(r'style=["\']([^"\']*)["\']', html_content, re.IGNORECASE) for style in styles: if 'color:' in style.lower() and 'background' in style.lower(): # Basic check for obvious low contrast patterns if '#fff' in style.lower() and any(c in style.lower() for c in ['#f', '#e']): print(f"POTENTIAL LOW CONTRAST: {style[:50]} on {page_url}") def _test_focus_management(self, page_url, html_content): """Test focus management elements""" # Check for skip links if not re.search(r']*href=["\']#[^"\']*["\'][^>]*>.*?skip.*?', html_content, re.IGNORECASE): print(f"MISSING SKIP LINK: {page_url}") # Check for positive tabindex (anti-pattern) positive_tabindex = re.findall(r'tabindex=["\']([1-9][0-9]*)["\']', html_content) if positive_tabindex: print(f"POSITIVE TABINDEX FOUND: {positive_tabindex} on {page_url}") def _test_keyboard_navigation(self, page_url, html_content): """Test keyboard navigation support""" # Check interactive elements for keyboard support interactive_elements = re.findall(r'<(button|a|input|select|textarea)[^>]*>', html_content, re.IGNORECASE) for element in interactive_elements: if 'onclick=' in element.lower() and element.lower().startswith(' { return new Promise((resolve) => { axe.run(config, (err, results) => { if (err) { resolve(null); } else { resolve(results); } }); }); } """, axe_config) return results except Exception as e: print(f"Error running axe scan: {str(e)}") return None def _process_axe_violations(self, page_url, violations): """Process and log axe violations""" processed_violations = [] for violation in violations: violation_data = { 'page': page_url, 'rule_id': violation.get('id', 'unknown'), 'impact': violation.get('impact', 'unknown'), 'description': violation.get('description', ''), 'help': violation.get('help', ''), 'help_url': violation.get('helpUrl', ''), 'nodes_count': len(violation.get('nodes', [])), 'tags': violation.get('tags', []) } processed_violations.append(violation_data) self.accessibility_violations.append(violation_data) # Log violation details impact = violation_data['impact'].upper() rule_id = violation_data['rule_id'] description = violation_data['description'] nodes_count = violation_data['nodes_count'] print(f"AXE VIOLATION [{impact}]: {rule_id} - {description} ({nodes_count} elements) on {page_url}") return processed_violations @task(1) @pw async def run_comprehensive_axe_scan(self, page: PageWithRetry): """Run comprehensive axe scan with all rules""" async with event(self, "Comprehensive Axe Scan"): await page.goto('/') # Inject axe-core await self._inject_axe_core(page) # Run comprehensive scan with all rules comprehensive_results = await page.evaluate(""" () => { return new Promise((resolve) => { // Run all axe rules axe.run((err, results) => { if (err) { resolve(null); } else { resolve({ violations: results.violations.length, passes: results.passes.length, incomplete: results.incomplete.length, inapplicable: results.inapplicable.length, total_rules: results.violations.length + results.passes.length + results.incomplete.length + results.inapplicable.length }); } }); }); } """) if comprehensive_results: print(f"COMPREHENSIVE AXE SCAN: {comprehensive_results['violations']} violations, " f"{comprehensive_results['passes']} passes, " f"{comprehensive_results['total_rules']} total rules checked") def on_stop(self): """Final axe accessibility report""" print("\n" + "="*50) print("AXE ACCESSIBILITY TEST COMPLETE") print("="*50) print(f"Pages tested: {len(self.discovered_pages) + 1}") print(f"Total axe violations: {len(self.accessibility_violations)}") if self.accessibility_violations: # Group violations by impact impact_counts = {} for violation in self.accessibility_violations: impact = violation['impact'] impact_counts[impact] = impact_counts.get(impact, 0) + 1 print(f"\nVIOLATIONS BY IMPACT:") for impact, count in sorted(impact_counts.items()): print(f" {impact.upper()}: {count}") print(f"\nTOP VIOLATIONS:") for violation in self.accessibility_violations[:5]: print(f"❌ [{violation['impact'].upper()}] {violation['rule_id']}: {violation['description']}") print(f" Page: {violation['page']} ({violation['nodes_count']} elements)") else: print("✅ No accessibility violations found!") ``` ## Comprehensive Axe-Core Site Scanning ```python theme={null} from locust import task from locust_plugins.users.playwright import PlaywrightUser, PageWithRetry, pw, event import json class ComprehensiveAxeSiteTester(PlaywrightUser): def on_start(self): """Initialize comprehensive axe site testing""" self.all_violations = [] self.pages_scanned = set() self.pages_to_scan = [] self.scan_summary = { 'total_pages': 0, 'total_violations': 0, 'critical_violations': 0, 'serious_violations': 0, 'moderate_violations': 0, 'minor_violations': 0 } @task(3) @pw async def comprehensive_site_scan(self, page: PageWithRetry): """Comprehensive axe scan across entire site""" if not self.pages_to_scan: await self._discover_all_pages(page) if self.pages_to_scan: page_url = self.pages_to_scan.pop(0) if page_url not in self.pages_scanned: await self._comprehensive_axe_test(page, page_url) self.pages_scanned.add(page_url) async def _discover_all_pages(self, page: PageWithRetry): """Discover all internal pages for comprehensive testing""" async with event(self, "Site Discovery"): await page.goto('/') # Get all internal links links = await page.locator('a[href]').all() for link in links[:50]: # Limit to 50 pages for comprehensive testing try: href = await link.get_attribute('href') if href and self._is_internal_page(href, page.url): normalized_url = self._normalize_url(href, page.url) if normalized_url and normalized_url not in self.pages_to_scan: self.pages_to_scan.append(normalized_url) except: pass print(f"Discovered {len(self.pages_to_scan)} pages for comprehensive axe scanning") def _is_internal_page(self, href, base_url): """Check if URL is internal page""" if not href: return False # Skip anchors, external protocols, and resources skip_patterns = ['#', 'mailto:', 'tel:', 'javascript:', '.pdf', '.jpg', '.jpeg', '.png', '.gif', '.css', '.js', '.zip', '.doc', '.xml'] if any(pattern in href.lower() for pattern in skip_patterns): return False # Check if internal if href.startswith('/'): return True elif href.startswith('http'): base_domain = base_url.split('/')[2] return base_domain in href else: return True # Relative links def _normalize_url(self, href, base_url): """Normalize URL for consistency""" if href.startswith('/'): return href elif href.startswith('http'): base_domain = base_url.split('/')[2] if base_domain in href: return '/' + href.split(base_domain, 1)[1].lstrip('/') else: # Relative URL return '/' + href.lstrip('./') return None async def _comprehensive_axe_test(self, page: PageWithRetry, page_url: str): """Comprehensive axe test on a single page""" async with event(self, f"COMPREHENSIVE AXE: {page_url}"): await page.goto(page_url) # Inject axe-core await self._inject_axe_core(page) # Run comprehensive axe scan with all rule sets axe_results = await self._run_comprehensive_axe_scan(page) if axe_results: self._process_comprehensive_results(page_url, axe_results) violations = axe_results.get('violations', []) if violations: critical = len([v for v in violations if v.get('impact') == 'critical']) serious = len([v for v in violations if v.get('impact') == 'serious']) failure_msg = f"❌ {len(violations)} axe violations ({critical} critical, {serious} serious)" raise Exception(failure_msg) else: print(f"✅ {page_url} - No violations found") async def _inject_axe_core(self, page: PageWithRetry): """Inject axe-core library""" await page.add_script_tag(url="https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.2/axe.min.js") await page.wait_for_function("typeof axe !== 'undefined'") async def _run_comprehensive_axe_scan(self, page: PageWithRetry): """Run comprehensive axe scan with all rules""" try: # Configure comprehensive scan axe_config = { 'runOnly': { 'type': 'tag', 'values': ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa', 'best-practice', 'experimental'] } } results = await page.evaluate(""" (config) => { return new Promise((resolve) => { axe.run(config, (err, results) => { if (err) { resolve(null); } else { resolve(results); } }); }); } """, axe_config) return results except Exception as e: print(f"Error running comprehensive axe scan: {str(e)}") return None def _process_comprehensive_results(self, page_url, results): """Process comprehensive axe results""" violations = results.get('violations', []) self.scan_summary['total_pages'] += 1 self.scan_summary['total_violations'] += len(violations) for violation in violations: impact = violation.get('impact', 'unknown') # Count by impact if impact == 'critical': self.scan_summary['critical_violations'] += 1 elif impact == 'serious': self.scan_summary['serious_violations'] += 1 elif impact == 'moderate': self.scan_summary['moderate_violations'] += 1 elif impact == 'minor': self.scan_summary['minor_violations'] += 1 # Store detailed violation violation_detail = { 'page': page_url, 'rule_id': violation.get('id'), 'impact': impact, 'description': violation.get('description'), 'help': violation.get('help'), 'help_url': violation.get('helpUrl'), 'tags': violation.get('tags', []), 'nodes': len(violation.get('nodes', [])) } self.all_violations.append(violation_detail) # Log violation print(f"AXE [{impact.upper()}]: {violation_detail['rule_id']} - {violation_detail['description']} " f"({violation_detail['nodes']} elements) on {page_url}") @task(1) @pw async def generate_accessibility_audit_report(self, page: PageWithRetry): """Generate comprehensive accessibility audit report""" if self.scan_summary['total_pages'] < 3: return async with event(self, "Accessibility Audit Report"): # Calculate compliance score total_possible_score = self.scan_summary['total_pages'] * 100 penalty_score = ( self.scan_summary['critical_violations'] * 10 + self.scan_summary['serious_violations'] * 5 + self.scan_summary['moderate_violations'] * 2 + self.scan_summary['minor_violations'] * 1 ) compliance_score = max(0, 100 - (penalty_score / total_possible_score * 100)) print(f"\nACCESSIBILITY AUDIT REPORT:") print(f"Pages scanned: {self.scan_summary['total_pages']}") print(f"Total violations: {self.scan_summary['total_violations']}") print(f"Critical: {self.scan_summary['critical_violations']}") print(f"Serious: {self.scan_summary['serious_violations']}") print(f"Moderate: {self.scan_summary['moderate_violations']}") print(f"Minor: {self.scan_summary['minor_violations']}") print(f"Compliance Score: {compliance_score:.1f}/100") def on_stop(self): """Final comprehensive report""" print("\n" + "="*60) print("COMPREHENSIVE AXE ACCESSIBILITY AUDIT COMPLETE") print("="*60) print(f"Total pages scanned: {len(self.pages_scanned)}") print(f"Total violations found: {len(self.all_violations)}") if self.all_violations: # Top violation types rule_counts = {} for violation in self.all_violations: rule_id = violation['rule_id'] rule_counts[rule_id] = rule_counts.get(rule_id, 0) + 1 print("\nTOP VIOLATION TYPES:") sorted_rules = sorted(rule_counts.items(), key=lambda x: x[1], reverse=True) for rule_id, count in sorted_rules[:10]: print(f" {rule_id}: {count} violations") # Most problematic pages page_counts = {} for violation in self.all_violations: page = violation['page'] page_counts[page] = page_counts.get(page, 0) + 1 print("\nMOST PROBLEMATIC PAGES:") sorted_pages = sorted(page_counts.items(), key=lambda x: x[1], reverse=True) for page, count in sorted_pages[:5]: print(f" {page}: {count} violations") else: print("✅ No accessibility violations found across entire site!") ``` ## Key Axe-Core Features 1. **Industry Standard**: Uses axe-core, the same engine used by accessibility professionals 2. **Comprehensive WCAG Coverage**: Tests WCAG 2.1 AA/AAA compliance automatically 3. **Detailed Violation Reports**: Specific rule violations with remediation guidance 4. **Real Browser Testing**: Accurate results from actual browser rendering 5. **Scalable Site Auditing**: Can scan entire websites automatically 6. **LoadForge Integration**: Violations show as failures in LoadForge reports 7. **Professional Reporting**: Impact levels (critical, serious, moderate, minor) This axe-core integration provides enterprise-grade accessibility testing that matches tools used by accessibility consultants and compliance teams. # Broken Links Checker Source: https://docs.loadforge.com/examples/qa-testing/broken-links-checker Crawl website and detect broken internal/external links with detailed reporting This guide demonstrates how to create a comprehensive broken links checker with LoadForge, crawling through all pages and detecting various types of link issues. ## Use Cases * Detecting 404 broken internal and external links * Finding redirect chains and infinite loops * Validating anchor links and fragments * Monitoring external link health over time * SEO link validation and reporting ## Simplified Internal Links Checker ```python theme={null} from locust import HttpUser, task, between import json import time import re from urllib.parse import urljoin, urlparse from collections import deque import logging class InternalLinksChecker(HttpUser): wait_time = between(0.5, 1) def on_start(self): """Initialize internal links checking""" self.visited_pages = set() self.pages_to_check = deque() self.broken_links = [] self.total_links_checked = 0 self.base_domain = None # Setup logging logging.basicConfig(level=logging.INFO) self.logger = logging.getLogger(__name__) # Start with homepage self._initialize_crawler() def _initialize_crawler(self): """Initialize the crawler with the homepage""" try: homepage_response = self.client.get('/', name="INIT: Homepage Check") if homepage_response.status_code == 200: self.base_domain = urlparse(self.client.base_url).netloc self.pages_to_check.append('/') self.logger.info(f"Starting internal links check for domain: {self.base_domain}") else: self.logger.error(f"Failed to access homepage: {homepage_response.status_code}") except Exception as e: self.logger.error(f"Error initializing crawler: {str(e)}") @task(10) def crawl_internal_pages(self): """Main crawling task - only checks internal links""" if not self.pages_to_check: return current_page = self.pages_to_check.popleft() if current_page in self.visited_pages: return self.visited_pages.add(current_page) try: response = self.client.get(current_page, name=f"CRAWL: {current_page}") if response.status_code == 200: self._extract_and_validate_internal_links(current_page, response.text) else: self._log_broken_link(current_page, current_page, response.status_code, f"Page not accessible: {response.status_code}") except Exception as e: self._log_broken_link(current_page, current_page, 0, f"Exception accessing page: {str(e)}") def _extract_and_validate_internal_links(self, source_page, html_content): """Extract and validate only internal page links (URLs only, no resources)""" # Only look for tags (actual page links) link_pattern = r']+href=["\']([^"\']+)["\'][^>]*>' # Find all tag links all_links = re.findall(link_pattern, html_content, re.IGNORECASE) # Filter to only internal page links and validate them for link in all_links: if self._is_internal_link(link) and not self._should_skip_link(link): # Only validate if it's a page link (not a resource) if self._is_page_link(link): self._validate_internal_link(source_page, link) # Add to crawl queue normalized_link = self._normalize_url(link) if normalized_link and normalized_link not in self.visited_pages: if normalized_link not in self.pages_to_check: self.pages_to_check.append(normalized_link) def _is_internal_link(self, link): """Check if a link is internal (same domain or relative)""" # Skip anchors, external protocols if link.startswith('#') or link.startswith('mailto:') or link.startswith('tel:'): return False # Relative paths are internal if link.startswith('/') or not link.startswith('http'): return True # Check if absolute URL is same domain if link.startswith('http'): return urlparse(link).netloc == self.base_domain return True def _is_page_link(self, link): """Check if link is a page (not a resource like image/css/js)""" # Skip common resource file extensions resource_extensions = {'.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.pdf', '.zip', '.mp4', '.mp3', '.woff', '.woff2', '.ttf', '.eot', '.webp', '.avif'} parsed_link = urlparse(link.lower()) path = parsed_link.path # Check if it ends with a resource extension for ext in resource_extensions: if path.endswith(ext): return False # Always consider these as pages (common blog/article patterns) page_patterns = [ r'/blog/', r'/articles?/', r'/posts?/', r'/news/', r'/(20\d{2})/', # Year pattern (2020, 2021, etc.) r'/category/', r'/tag/', ] for pattern in page_patterns: if re.search(pattern, path): return True return True def _should_skip_link(self, link): """Determine if a link should be skipped entirely""" skip_patterns = [ r'^mailto:', r'^tel:', r'^javascript:', r'^data:', r'^#$', # Empty anchor r'^\s*$', # Empty or whitespace ] for pattern in skip_patterns: if re.match(pattern, link, re.IGNORECASE): return True return False def _validate_internal_link(self, source_page, link): """Validate an internal link and log any issues""" self.total_links_checked += 1 normalized_url = self._normalize_url(link) if not normalized_url: self._log_broken_link(source_page, link, 0, "Invalid URL format") return try: with self.client.get(normalized_url, name=f"LINK: {normalized_url}", catch_response=True) as response: if response.status_code == 404: self._log_broken_link(source_page, link, 404, "Page not found") response.failure("❌ 404 Not Found") elif response.status_code >= 500: self._log_broken_link(source_page, link, response.status_code, "Server error") response.failure(f"❌ Server Error {response.status_code}") elif response.status_code >= 400: self._log_broken_link(source_page, link, response.status_code, "Client error") response.failure(f"❌ Client Error {response.status_code}") else: response.success() except Exception as e: self._log_broken_link(source_page, link, 0, f"Request failed: {str(e)}") def _log_broken_link(self, source_page, broken_link, status_code, reason): """Log broken link with detailed information""" broken_link_info = { 'source_page': source_page, 'broken_link': broken_link, 'status_code': status_code, 'reason': reason, 'timestamp': time.time() } self.broken_links.append(broken_link_info) # Log to console and LoadForge error_msg = f"BROKEN LINK: {broken_link} (Status: {status_code}) found on page: {source_page} - {reason}" self.logger.error(error_msg) print(error_msg) def _normalize_url(self, link): """Normalize URL for consistent checking""" try: # Handle relative paths if link.startswith('/'): return link elif link.startswith('http'): parsed = urlparse(link) if parsed.netloc == self.base_domain: return parsed.path + ('?' + parsed.query if parsed.query else '') return None # External link, skip else: # Relative link - normalize to absolute path return '/' + link.lstrip('./') except Exception: return None @task(1) def report_status(self): """Print status to console (no API submission needed)""" if len(self.visited_pages) < 3: # Wait until we have some data return print(f"STATUS: {len(self.visited_pages)} pages crawled, " f"{self.total_links_checked} links checked, " f"{len(self.broken_links)} broken links found") def on_stop(self): """Final summary when test completes""" print("\n" + "="*50) print("INTERNAL LINKS CHECK COMPLETE") print("="*50) print(f"Pages crawled: {len(self.visited_pages)}") print(f"Total links checked: {self.total_links_checked}") print(f"Broken links found: {len(self.broken_links)}") if self.broken_links: print(f"\nBROKEN LINKS FOUND ({len(self.broken_links)}):") print("-" * 40) for link_info in self.broken_links: print(f"❌ {link_info['broken_link']} (HTTP {link_info['status_code']})") print(f" Found on: {link_info['source_page']}") print(f" Reason: {link_info['reason']}") print() else: print("✅ No broken links found!") ``` ## Comprehensive Broken Links Checker ```python theme={null} from locust import HttpUser, task, between import json import time import random import re from urllib.parse import urljoin, urlparse, urlunparse from collections import defaultdict, deque import requests class BrokenLinksChecker(HttpUser): wait_time = between(0.5, 2) def on_start(self): """Initialize broken links checking""" self.visited_pages = set() self.pages_to_check = deque() self.broken_links = [] self.redirect_chains = [] self.external_links = {} self.anchor_links = {} self.link_stats = defaultdict(int) self.base_domain = None # Start with homepage self._initialize_crawler() def _initialize_crawler(self): """Initialize the crawler with the homepage""" homepage_response = self.client.get('/', name="homepage_initial_check") if homepage_response.status_code == 200: self.base_domain = urlparse(self.client.base_url).netloc self.pages_to_check.append('/') print(f"Starting broken links check for domain: {self.base_domain}") else: print(f"Failed to access homepage: {homepage_response.status_code}") @task(5) def crawl_and_check_links(self): """Main crawling task to check links on pages""" if not self.pages_to_check: return current_page = self.pages_to_check.popleft() if current_page in self.visited_pages: return self.visited_pages.add(current_page) # Get the page content response = self.client.get(current_page, name="crawl_page_for_links") if response.status_code == 200: self._extract_and_validate_links(current_page, response.text) else: self._record_broken_link(current_page, response.status_code, "Page not accessible") def _extract_and_validate_links(self, page_url, html_content): """Extract all links from HTML and validate them""" # Find all links in the HTML link_patterns = [ r']+href=["\']([^"\']+)["\'][^>]*>', # r']+href=["\']([^"\']+)["\'][^>]*>', # r']+src=["\']([^"\']+)["\'][^>]*>', # r']+src=["\']([^"\']+)["\'][^>]*>', # ', html_content, re.IGNORECASE | re.DOTALL) image_in_structured_data = False for script in json_ld_scripts: if 'image' in script.lower(): image_in_structured_data = True break if image_in_structured_data: print("✅ Images found in structured data") else: print("⚠️ No images in structured data (SEO opportunity)") @task(1) def generate_advanced_performance_report(self): """Generate advanced image performance report""" advanced_report = { 'timestamp': time.time(), 'modern_format_support': { 'webp_tested': True, 'avif_tested': True, 'optimization_recommendations': [] }, 'cdn_performance': { 'caching_effective': True, 'cdn_domains_checked': self.CDN_DOMAINS }, 'responsive_images': { 'implementation_detected': True, 'picture_elements_found': True }, 'accessibility_score': 85, # Would be calculated from actual checks 'seo_optimization': { 'structured_data': True, 'alt_text_quality': 'good' } } self.client.post('/api/qa/advanced-image-performance-report', json=advanced_report, name="submit_advanced_performance_report") print("ADVANCED REPORT: Performance and optimization analysis complete") ``` ## Key Validation Features 1. **Simple Version**: Basic broken images, alt text, and configurable size limits 2. **Comprehensive Version**: Full validation including external images, responsive images, and detailed reporting 3. **Advanced Version**: Modern format support, CDN performance, SEO optimization, and accessibility testing 4. **Configurable Parameters**: Easy-to-modify size limits and validation settings at the top of each script 5. **Progressive Complexity**: Start simple, add features as needed This guide provides three levels of image validation to match your testing needs and technical requirements. # Simple Form Validation Source: https://docs.loadforge.com/examples/qa-testing/simple-form-validation Basic form validation testing to check form fields, validation rules, error handling, and submission flows This guide shows how to test form validation logic and error handling. Perfect for validating form fields, submission flows, and user input validation. ## Use Cases * Test form field validation rules * Validate error messages and handling * Test form submission flows * Check input sanitization and security ## Simple Implementation ```python theme={null} from locust import task, HttpUser import random import json class FormValidationUser(HttpUser): def on_start(self): # Common form endpoints to test self.forms = { "/contact": { "required_fields": ["name", "email", "message"], "optional_fields": ["phone", "company"], "validation_rules": { "email": "email_format", "phone": "phone_format" } }, "/signup": { "required_fields": ["username", "email", "password"], "optional_fields": ["first_name", "last_name"], "validation_rules": { "email": "email_format", "password": "min_length_8", "username": "alphanumeric" } }, "/profile": { "required_fields": ["first_name", "last_name"], "optional_fields": ["bio", "website", "location"], "validation_rules": { "website": "url_format", "bio": "max_length_500" } } } # Test data for validation self.test_data = { "valid": { "name": "John Doe", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "username": "johndoe123", "password": "SecurePass123!", "phone": "+1-555-123-4567", "message": "This is a test message", "website": "https://example.com", "bio": "Software developer with 5 years experience" }, "invalid": { "email": ["invalid-email", "@example.com", "test@", "plaintext"], "password": ["123", "short", ""], "phone": ["123", "invalid-phone", ""], "website": ["not-a-url", "ftp://invalid", ""], "username": ["", "user@name", "user name", "a"] } } @task(4) def test_valid_form_submission(self): """Test form submission with valid data""" form_path = random.choice(list(self.forms.keys())) form_config = self.forms[form_path] # Build valid form data form_data = {} # Add required fields for field in form_config["required_fields"]: if field in self.test_data["valid"]: form_data[field] = self.test_data["valid"][field] # Add some optional fields for field in form_config["optional_fields"]: if field in self.test_data["valid"] and random.choice([True, False]): form_data[field] = self.test_data["valid"][field] with self.client.post( form_path, data=form_data, name=f"Valid Form - {form_path}" ) as response: if response.status_code in [200, 201, 302]: print(f"Valid form submission {form_path}: SUCCESS ({response.status_code})") # Check for success indicators if response.status_code == 200: success_indicators = ["success", "thank you", "submitted", "received"] response_text = response.text.lower() success_found = any(indicator in response_text for indicator in success_indicators) if success_found: print(f"Success message detected in {form_path}") else: print(f"No clear success message in {form_path}") elif response.status_code == 400: print(f"Valid form submission {form_path}: Validation error (unexpected)") response.failure("Valid data rejected") else: response.failure(f"Unexpected response: {response.status_code}") @task(3) def test_missing_required_fields(self): """Test form submission with missing required fields""" form_path = random.choice(list(self.forms.keys())) form_config = self.forms[form_path] if not form_config["required_fields"]: return # Skip if no required fields # Build form data missing one required field form_data = {} missing_field = random.choice(form_config["required_fields"]) for field in form_config["required_fields"]: if field != missing_field and field in self.test_data["valid"]: form_data[field] = self.test_data["valid"][field] with self.client.post( form_path, data=form_data, name=f"Missing Field - {form_path}" ) as response: if response.status_code == 400: print(f"Missing field test {form_path}: Correctly rejected ({missing_field})") # Check for error message about missing field if response.text: error_indicators = ["required", "missing", "field", missing_field] response_text = response.text.lower() error_found = any(indicator in response_text for indicator in error_indicators) if error_found: print(f"Error message detected for missing {missing_field}") else: print(f"No clear error message for missing {missing_field}") elif response.status_code in [200, 201, 302]: print(f"Missing field test {form_path}: Unexpectedly accepted") response.failure(f"Form accepted without required field: {missing_field}") else: response.failure(f"Unexpected response: {response.status_code}") @task(3) def test_invalid_field_formats(self): """Test form submission with invalid field formats""" form_path = random.choice(list(self.forms.keys())) form_config = self.forms[form_path] # Find a field with validation rules validation_fields = list(form_config.get("validation_rules", {}).keys()) if not validation_fields: return # Skip if no validation rules test_field = random.choice(validation_fields) # Build form data with invalid value for test field form_data = {} # Add required fields (valid values) for field in form_config["required_fields"]: if field == test_field: # Use invalid value for test field if test_field in self.test_data["invalid"]: form_data[field] = random.choice(self.test_data["invalid"][test_field]) else: form_data[field] = "invalid_value" elif field in self.test_data["valid"]: form_data[field] = self.test_data["valid"][field] with self.client.post( form_path, data=form_data, name=f"Invalid Format - {form_path}" ) as response: if response.status_code == 400: print(f"Invalid format test {form_path}: Correctly rejected ({test_field})") # Check for specific validation error validation_keywords = ["invalid", "format", "valid", test_field] response_text = response.text.lower() error_found = any(keyword in response_text for keyword in validation_keywords) if error_found: print(f"Validation error message detected for {test_field}") else: print(f"No clear validation error for {test_field}") elif response.status_code in [200, 201, 302]: print(f"Invalid format test {form_path}: Unexpectedly accepted") response.failure(f"Invalid {test_field} format accepted") else: response.failure(f"Unexpected response: {response.status_code}") @task(2) def test_empty_form_submission(self): """Test form submission with completely empty data""" form_path = random.choice(list(self.forms.keys())) with self.client.post( form_path, data={}, name=f"Empty Form - {form_path}" ) as response: if response.status_code == 400: print(f"Empty form test {form_path}: Correctly rejected") # Check for multiple field errors error_indicators = ["required", "missing", "field", "empty"] response_text = response.text.lower() error_count = sum(1 for indicator in error_indicators if indicator in response_text) if error_count >= 2: print(f"Multiple validation errors detected (good)") else: print(f"Limited error feedback for empty form") elif response.status_code in [200, 201, 302]: print(f"Empty form test {form_path}: Unexpectedly accepted") response.failure("Empty form accepted") else: response.failure(f"Unexpected response: {response.status_code}") @task(2) def test_field_length_limits(self): """Test form fields with length limits""" form_path = random.choice(list(self.forms.keys())) form_config = self.forms[form_path] # Test with very long values long_values = { "name": "A" * 1000, "email": "a" * 500 + "@example.com", "message": "This is a very long message. " * 100, "bio": "Long bio content. " * 200, "username": "a" * 500 } # Build form data with one long field form_data = {} test_field = None for field in form_config["required_fields"]: if field in long_values: form_data[field] = long_values[field] test_field = field break elif field in self.test_data["valid"]: form_data[field] = self.test_data["valid"][field] if not test_field: return # Skip if no testable field with self.client.post( form_path, data=form_data, name=f"Length Limit - {form_path}" ) as response: if response.status_code == 400: print(f"Length limit test {form_path}: Correctly rejected ({test_field})") # Check for length-related error length_keywords = ["long", "length", "limit", "maximum", "characters"] response_text = response.text.lower() length_error = any(keyword in response_text for keyword in length_keywords) if length_error: print(f"Length validation error detected for {test_field}") else: print(f"Generic error for length violation on {test_field}") elif response.status_code in [200, 201, 302]: print(f"Length limit test {form_path}: Accepted (no length limit)") else: response.failure(f"Unexpected response: {response.status_code}") @task(1) def test_special_characters(self): """Test form handling of special characters""" form_path = random.choice(list(self.forms.keys())) form_config = self.forms[form_path] # Special characters to test special_chars = { "name": "John O'Connor-Smith", "message": "Test with and special chars: àáâãäå", "bio": "Bio with émojis 🚀 and spëcial chars", "company": "Smith & Jones Co., Ltd." } # Build form data with special characters form_data = {} for field in form_config["required_fields"]: if field in special_chars: form_data[field] = special_chars[field] elif field in self.test_data["valid"]: form_data[field] = self.test_data["valid"][field] with self.client.post( form_path, data=form_data, name=f"Special Chars - {form_path}" ) as response: if response.status_code in [200, 201, 302, 400]: print(f"Special chars test {form_path}: {response.status_code}") # Check that dangerous content is handled if "