Hands-on Rust Test Series: #3 – Mock External API Dependencies with Wiremock

The Context

Modern applications frequently communicate with 3rd party APIs. In the provided POC, the foo_service.rs and integration tests interact with external URLs. Testing these interactions is critical, as data can change or fail when transferred across modules.


The Problem

Relying on a real 3rd party API during testing is a major anti-pattern. Real APIs can be slow, require authentication, have rate limits, or even be offline, which makes your tests flaky and non-repeatable. Additionally, you cannot easily force a 3rd party API to return a “500 Internal Server Error” to test your application’s exception handling.


The Solution

Important Disclaimer: This is a Proof of Concept (PoC) built for a specific course need. If you find this code useful and decide to use it, you do so entirely at your own risk. The authors decline any responsibility for its use.


Wiremock allows you to spin up a local HTTP server that “simulates” the behavior of external services. It is recommended as a best practice for integration tests to replace real hosts with “in-memory” or local mock servers.


Before we dive in, the full source code is available on GitHub: 👉 https://github.com/gabo-gil-playground/rust-poc-unit-and-integration-test-sample

Here are the core dependencies in Cargo.toml:

[dev-dependencies] # development dependencies - should not be exported

# mock dependencies
mockall = { version = "=0.14.0" }

# parametrized test cases dependencies
rstest = { version = "=0.26.1" }

# run test sequencially
serial_test = { version = "=3.3.1" }

# unit and integration tests util / tools dependencies
tower = { version = "=0.5.3", features = ["util", "timeout"] }

# http mock server for external API calls
wiremock = { version = "=0.6.5" }

Pro-tip #1: Mounting Expectations

In the Arrange phase, you “mount” a response onto the MockServer. You can specify that a GET request to a specific path should return a specific JSON body. This ensures your service receives exactly what it needs to proceed with the Act step.


// src/service/foo_service.rs

async fn when_get_result_from_third_party_api_should_return_string(
	#[case] test_case_status: u16,
) {
	let mock_server = wiremock::MockServer::start().await; // creates with random port
	mock_server
		.register(create_mock_endpoint(
			MOCK_URL_PATH,
			test_case_status,
			Some(MOCK_API_RESPONSE),
		))
		.await;

	...

	mock_server.reset().await; // reset mock server and clean the context

pub fn create_mock_endpoint(path: &str, status: u16, response: Option<&str>) -> Mock {
	let mock_response =
		ResponseTemplate::new(status).set_body_bytes(response.unwrap_or_default());
	Mock::given(matchers::path_regex(path)).respond_with(mock_response)
}

Pro-tip #2: Simulating Latency and Errors

Wiremock isn’t just for happy paths. You can configure it to return errors or delayed responses. This is the only way to reliably verify that your code’s “Inadequate exception handling” is addressed before reaching production:


// src/service/foo_service.rs

#[rstest]
#[case(302)]
#[case(400)]
#[case(401)]
#[case(404)]
#[case(500)]
#[tokio::test]
async fn when_get_result_from_third_party_api_and_response_is_not_success_should_return_error(
        #[case] test_case_status: u16,
) {
	let mock_server = wiremock::MockServer::start().await; // creates with random port
	mock_server
		.register(create_mock_endpoint(MOCK_URL_PATH, test_case_status, None))
		.await;

	let mock_server_host = mock_server.uri();
	let mock_api_url = format!("{mock_server_host}/{MOCK_URL_PATH}");

	let foo_service = FooService {};
	let result = foo_service
		.get_result_from_third_party_api(mock_api_url)
		.await;

	assert_eq!((), result.unwrap_err());

	mock_server.reset().await; // reset mock server and clean the context
}

Pro-tip #3: Verifying Calls

Beyond just providing data, Wiremock can Assert that your application actually made the call. You can verify that your code sent the correct headers or payload to the 3rd party service, ensuring the integration logic is sound.


To run the code, you just need to run cargo run via your terminal or favorite IDE (I personally recommend RustRover). For more details, please, check the README.md file.


Summary of Steps

  1. Start a MockServer in your test’s Arrange phase.
  2. Use Mock::given to define matchers (like method and path).
  3. Use .respond_with to define the simulated API response.
  4. Point your application’s “third_party_url” to the server.uri().
  5. Act and Assert on the resulting behavior.


The Results

Wiremock turns unpredictable external dependencies into stable, repeatable test conditions. This allows you to catch system-level issues early in the development cycle, where they are much cheaper to fix than in the maintenance phase.



Do you need help modernizing your legacy applications, optimizing your architecture or improving your development workflows? Let’s connect on LinkedIn or check out my services on Upwork.


Comments

Leave a Reply

Discover more from Gabo Gil

Subscribe now to keep reading and get access to the full archive.

Continue reading