Skip to main content
Glama

Blockscout MCP Server

Official
220-integration-testing-guidelines.mdc12.9 kB
--- description: globs: tests/integration/* alwaysApply: false --- # Integration Testing Guidelines This document provides detailed guidelines for writing effective integration tests that verify our interaction with live external APIs. Unlike unit tests which focus on logic with mocked data, integration tests ensure the "contract" between our tools and external services remains valid. ## Categories of Integration Tests ### Category 1: Helper-Level Integration Tests (Connectivity & Basic Contract) These tests target the low-level helper functions in `tools/common.py` (e.g., `make_blockscout_request`, `get_blockscout_base_url`). - **Purpose:** To verify basic network connectivity and ensure the fundamental HTTP request/response cycle with each external service is working. - **Location:** `tests/integration/test_common_helpers.py`. - **What to Assert:** - The request was successful (no HTTP errors). - The top-level structure of the response is as expected (e.g., `isinstance(response, list)`). - Presence of a few key, stable fields to confirm we're hitting the right endpoint. **Example:** ```python import pytest from blockscout_mcp_server.tools.common import make_blockscout_request, get_blockscout_base_url @pytest.mark.integration async def test_make_blockscout_request_connectivity(): """Test basic connectivity to Blockscout API.""" base_url = await get_blockscout_base_url("1") # Ethereum mainnet response = await make_blockscout_request( base_url=base_url, api_path="/api/v2/blocks", params={"limit": 1} ) # Assert basic structure assert isinstance(response, dict) assert "items" in response assert len(response["items"]) > 0 # Assert key fields are present block = response["items"][0] assert "hash" in block assert "number" in block ``` ### Category 2: Tool-Level Integration Tests (Data Extraction & Schema Validation) These tests target the high-level MCP tool functions themselves (e.g., `get_latest_block`, `get_tokens_by_address`). - **Purpose:** To validate the "contract" between our tool's data processing logic and the live API's response schema. They ensure that the fields our tools *extract and transform* are still present and correctly structured in the live data. This protects against breaking changes in the API schema. - **Location:** Domain-specific folders under `tests/integration/` with modules named `test_<tool_name>_real.py` (for example, `tests/integration/block/test_get_latest_block_real.py`). Each MCP tool must have a dedicated module that contains all integration scenarios for that tool. Do not combine multiple tools in a single module. - **What to Call:** The actual MCP tool function (e.g., `await get_latest_block(chain_id="1", ctx=mock_ctx)`). - **What to Assert:** - Focus on the **final, processed result** returned by the tool. - Verify that the extracted data has the correct type and format (e.g., `assert isinstance(result["block_number"], int)`). - For lists, check that items in the list contain the expected processed fields (e.g., `assert "address" in item`). - For tools with string formatting (like pagination hints), assert that the key substrings are present in the final output. - **What to Avoid:** Do not re-test complex formatting logic already covered by unit tests. The focus is on verifying the *data extraction* was successful. **Example:** ```python import pytest from blockscout_mcp_server.tools.block.get_latest_block import get_latest_block @pytest.mark.integration async def test_get_latest_block_data_extraction(mock_ctx): """Test that get_latest_block extracts expected fields from live API.""" result = await get_latest_block(chain_id="1", ctx=mock_ctx) # Assert the tool returns a ToolResponse structure assert hasattr(result, 'data') assert "block_number" in result.data assert "hash" in result.data assert "timestamp" in result.data # Assert data types are correct assert isinstance(result.data["block_number"], int) assert isinstance(result.data["hash"], str) assert result.data["hash"].startswith("0x") # Assert reasonable data ranges assert result.data["block_number"] > 0 assert len(result.data["hash"]) == 66 # 0x + 64 hex chars ``` - **What to Avoid:** Do not re-test complex formatting logic already covered by unit tests. The focus is on verifying the *data extraction* was successful. #### Handling Paginated Data For tools that return paginated data, integration tests must be robust enough to find specific data patterns that may not be on the first page. Instead of assuming data is in the initial response, tests should loop through pages. **Best Practice:** - Use a `while` or `for` loop with a `max_pages_to_check` limit to prevent infinite loops. - In each iteration, call the tool with the current `cursor`. - Inspect the response for the target data. If found, break the loop. - If not found, parse the new cursor from the response string and continue to the next page. - If the data is not found after checking the maximum number of pages, the test should be skipped with a clear message. **Example:** ```python import pytest @pytest.mark.integration async def test_tool_with_pagination_search(mock_ctx): """Test that we can find specific data by searching across pages.""" MAX_PAGES_TO_CHECK = 5 cursor = None found_item = None for _ in range(MAX_PAGES_TO_CHECK): # Call the paginated tool result = await some_paginated_tool(chain_id="1", cursor=cursor, ctx=mock_ctx) # Logic to search for the specific item in the ToolResponse data for item in result.data: # if item_matches_criteria: # found_item = item # break pass if found_item: break # Extract next cursor from structured pagination if more pages exist if result.pagination: cursor = result.pagination.next_call.params["cursor"] else: break if not found_item: pytest.skip(f"Could not find target item within {MAX_PAGES_TO_CHECK} pages.") # Assertions on the found_item assert found_item["some_field"] == "expected_value" ``` **Testing Pagination and Cursors:** For tools that support cursor-based pagination, a specific two-step test is required to validate the full lifecycle of the feature. - **Purpose:** To verify that a cursor generated from a first call can be successfully used in a second call to retrieve the next page of data. - **Process:** 1. Make an initial call to the tool without a cursor. 2. Extract the cursor string from the response's pagination hint. 3. Make a second call to the same tool, passing the extracted cursor. 4. Assert that the second call succeeds and that its data is different from the first call's data. **Example:** ```python import pytest from blockscout_mcp_server.tools.address_tools import get_tokens_by_address @pytest.mark.integration async def test_get_tokens_by_address_pagination_integration(mock_ctx): """Tests that get_tokens_by_address can use a cursor to fetch a second page.""" # ARRANGE: Use a stable address known to have many results. address = "0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503" # Binance Wallet chain_id = "1" # ACT 1: Get the first page. first_page_response = await get_tokens_by_address(chain_id=chain_id, address=address, ctx=mock_ctx) # ASSERT 1: Check for and extract the next call information. assert first_page_response.pagination is not None, "Pagination info is missing." next_call_info = first_page_response.pagination.next_call # ACT 2: Use the structured parameters to get the next page. second_page_response = await get_tokens_by_address(**next_call_info.params, ctx=mock_ctx) # ASSERT 2: Verify the second page. assert len(second_page_response.data) > 0 assert first_page_response.data[0] != second_page_response.data[0] ``` ## Testing the `direct_api_call` Dispatcher To test that the dispatcher correctly routes to a handler: - **Location**: `tests/integration/direct_api/test_<handler_name>_real.py`. - **Method**: Call the main `direct_api_call` tool with an `endpoint_path` that should trigger the handler. - **Assertion**: Assert that the returned `ToolResponse` contains the specific data model processed by the handler (e.g., `isinstance(result.data, list)` and `isinstance(result.data[0], AddressLogItem)`), not the generic `DirectApiData`. This verifies the end-to-end dispatch flow. ## General Rules for All Integration Tests ### Use Stable Targets Always test against non-volatile data points to ensure tests are reliable across different runs: - **Historical blocks:** Use well-known blocks (e.g., Genesis block, specific milestone blocks) - **Famous addresses:** Use well-known contract addresses (e.g., USDC, prominent DeFi protocols) - **ENS names:** Use established ENS names that are unlikely to change **Examples:** ```python # Good: Historical data that won't change GENESIS_BLOCK_HASH = "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" USDC_CONTRACT = "0xa0b86a33e6ac4454df1b3b7df6b2e55e5ef2a74f" VITALIK_ENS = "vitalik.eth" # Bad: Latest/current data that changes frequently await get_latest_block(...) # Block data changes every ~12 seconds ``` ### Integration Test Markers Every integration test function **must** be decorated with `@pytest.mark.integration`: ```python @pytest.mark.integration async def test_some_integration_scenario(): # Test implementation pass ``` This allows running integration tests separately: ```bash # Run only integration tests pytest -m integration # Run everything except integration tests pytest -m "not integration" ``` **Important:** Always use the `-v` (verbose) flag when running integration tests to see the reason for any skipped tests: ```bash pytest -m integration -v ``` This verbose output will show you why specific tests were skipped (e.g., network connectivity issues, missing API keys, or external service unavailability), which is crucial for understanding the test results and debugging integration issues. ### Error Handling Integration tests should be resilient to temporary network issues: ```python import pytest import httpx @pytest.mark.integration async def test_with_retry_logic(): """Integration test with basic retry for network issues.""" max_retries = 3 for attempt in range(max_retries): try: result = await some_tool_function(...) break # Success, exit retry loop except (httpx.TimeoutException, httpx.ConnectError) as e: if attempt == max_retries - 1: pytest.skip(f"Network connectivity issue after {max_retries} attempts: {e}") continue # Proceed with assertions assert result is not None ``` ### Avoid Hardcoding Environment-Specific URLs When asserting on output that contains a URL resolved by the server, **DO NOT** hardcode the URL in your test. The server dynamically resolves URLs (e.g., using `get_blockscout_base_url`), and these resolved values can change. Instead, the test itself should call the same helper to get the expected URL and use that variable in the assertion. This makes the test resilient to changes in the underlying service infrastructure. **Incorrect (Brittle):** ```python @pytest.mark.integration async def test_tool_with_hardcoded_url(mock_ctx): result = await some_tool_that_generates_a_url(chain_id="1", ...) # This will break if the resolved URL for chain 1 changes. assert "https://eth.blockscout.com/some/path" in result ``` **Correct (Robust):** ```python from blockscout_mcp_server.tools.common import get_blockscout_base_url @pytest.mark.integration async def test_tool_with_dynamic_url(mock_ctx): base_url = await get_blockscout_base_url("1") result = await some_tool_that_generates_a_url(chain_id="1", ...) # This test is resilient to changes in the resolved URL. assert f"{base_url}/some/path" in result ``` ## File Size Limitations **Integration test files must not exceed 500 LOC.** If a file approaches this limit, split tests into multiple focused modules such as `test_get_transaction_logs_real_part1.py` and `test_get_transaction_logs_real_part2.py` to maintain readability and logical organization. ## Test Organization - Group related tests using descriptive class names or clear function naming patterns. - Use descriptive test names that indicate what contract/schema is being validated - Separate helper-level tests from tool-level tests into different files when appropriate - Include clear comments explaining why specific test data was chosen (especially for stable targets)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/blockscout/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server