| name | testing-external-dependencies |
| description | Strategies for testing applications with expensive or complex external dependencies (APIs, ML models, media processing tools) |
| domain | testing |
| confidence | medium |
| source | earned |
Context
Applications that integrate with external systems face unique testing challenges:
- External APIs may cost money per call, have rate limits, or require authentication
- ML models may require large downloads or expensive compute
- Media processing tools (FFmpeg, video/audio encoders) may be slow
- Third-party services may have unreliable uptime or limited sandbox environments
Traditional "test everything with real dependencies" approaches are impractical. A multi-tiered strategy balances coverage, speed, and cost.
Patterns
1. Three-Tier Test Architecture
Unit Tests - Fast, isolated, no external dependencies
- Mock all external services using interfaces
- Test business logic, validation, error handling
- Run on every commit (< 30 seconds total)
- Target: 80%+ coverage of core logic
Integration Tests - Real dependencies, controlled scope
- Use real database (SQLite in-memory or temp files)
- Use real tools with small inputs (FFmpeg on 5-second videos)
- Mock expensive/unreliable external APIs (LLMs, payment gateways)
- Tagged for selective execution:
[Trait("Category", "Integration")]
- Run on PR validation
E2E Tests - Full workflows, optional external services
- Test critical user journeys end-to-end
- Use fake providers or mocked APIs to avoid costs
- Run nightly or on-demand
- Tagged:
[Trait("Category", "E2E")], [Trait("Speed", "Slow")]
2. Test Trait Taxonomy for Selective Execution
Use xUnit traits to enable targeted test runs:
[Trait("Category", "Unit")]
[Trait("Category", "Integration")]
[Trait("Speed", "Slow")]
[Trait("External", "True")]
[Trait("RequiresAuth", "True")]
[Trait("Expensive", "True")]
CI pipeline examples:
dotnet test --filter "Category=Unit"
dotnet test --filter "Category=Unit|Category=Integration" --filter "External!=True"
dotnet test
3. Interface Extraction for Testability
Wrap external dependencies behind interfaces for mocking:
public class AudioService
{
public async Task ExtractAudio(string videoPath)
{
await FFMpegArguments.FromFileInput(videoPath)...
}
}
public interface IFFmpegService
{
Task<bool> ExtractAudioAsync(string input, string output);
}
public class AudioService
{
private readonly IFFmpegService _ffmpeg;
public AudioService(IFFmpegService ffmpeg)
{
_ffmpeg = ffmpeg;
}
public async Task ExtractAudio(string videoPath)
{
await _ffmpeg.ExtractAudioAsync(videoPath, outputPath);
}
}
var mockFFmpeg = new Mock<IFFmpegService>();
mockFFmpeg.Setup(x => x.ExtractAudioAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(true);
var realFFmpeg = new FFmpegService();
4. Fake Providers for Predictable Testing
Create simplified "fake" implementations for complex providers:
public class FakeLlmProvider : ILlmProvider
{
private readonly List<Segment> _predefinedSegments;
public FakeLlmProvider(List<Segment>? segments = null)
{
_predefinedSegments = segments ?? new List<Segment>
{
new Segment { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromSeconds(30) }
};
}
public Task<(bool Success, IEnumerable<Segment>? Segments, string? Error)>
GenerateSegmentsAsync(...)
{
return Task.FromResult<(bool, IEnumerable<Segment>?, string?)>(
(true, _predefinedSegments, null));
}
}
Benefits:
- Deterministic test results (no AI variance)
- No API costs
- Fast execution
- Can simulate error conditions
5. Test Data Management
For media processing applications:
Small Sample Files - Check into repository (or Git LFS)
short-test.mp4 (5 seconds) - for fast integration tests
tiny-audio.wav (1 second) - for unit tests with real file I/O
sample-transcript.json - known-good output for validation
Programmatic Generation - Create test data in code
public static class TestData
{
public static byte[] CreateSilentWavFile(TimeSpan duration)
{
}
public static Project CreateTestProject(string name = "Test") =>
new Project { Name = name, };
}
Snapshot Testing - For complex outputs (Verify library)
[Fact]
public async Task GenerateSegments_ProducesExpectedStructure()
{
var result = await service.GenerateSegmentsAsync(...);
await Verify(result.Segments);
}
6. Environment-Based Test Configuration
Use environment variables for optional integration tests:
public class OpenAiIntegrationTests
{
[Fact]
public async Task CallRealOpenAI_ReturnsValidResponse()
{
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
if (string.IsNullOrEmpty(apiKey))
{
Assert.Skip("OPENAI_API_KEY not set - skipping integration test");
}
}
}
Or use Traits + test configuration:
[Trait("External", "True")]
[Trait("RequiresAuth", "True")]
public async Task CallRealAPI_Works()
{
}
Examples
Example: Testing Video Transcription Service
[Fact]
public async Task TranscribeAsync_WhenProviderReturnsError_PropagatesError()
{
var mockProvider = new Mock<ITranscriptProvider>();
mockProvider.Setup(x => x.TranscribeAsync(It.IsAny<string>()))
.ReturnsAsync((false, null, "API Error"));
var service = new TranscriptService(mockProvider.Object);
var result = await service.TranscribeAsync("test.wav");
result.Success.Should().BeFalse();
result.Error.Should().Contain("API Error");
}
[Trait("Category", "Integration")]
[Trait("RequiresModel", "True")]
[Trait("Speed", "Slow")]
public async Task TranscribeAsync_WithRealWhisper_ProducesTranscript()
{
var provider = new WhisperTranscriptProvider();
var testAudioPath = "Fixtures/tiny-audio.wav";
var result = await provider.TranscribeAsync(testAudioPath);
result.Success.Should().BeTrue();
result.Transcript.Should().NotBeNullOrEmpty();
}
[Trait("Category", "E2E")]
public async Task CompleteTranscriptionWorkflow_Succeeds()
{
var fakeProvider = new FakeTranscriptProvider("Test transcript");
var service = new TranscriptService(fakeProvider);
}
Anti-Patterns
Don't: Make all tests depend on real external services
❌ Every test calls OpenAI API → expensive, slow, flaky
✅ Unit tests mock APIs, integration tests are tagged and optional
Don't: Skip integration tests entirely
❌ Only unit tests with mocks → may not work with real services
✅ Selective integration tests with real dependencies on small inputs
Don't: Check secrets into tests
❌ var apiKey = "sk-abc123..." in test code
✅ Use environment variables, user secrets, or CI secret management
Don't: Make slow tests mandatory for local dev
❌ dotnet test requires downloading 5GB model and takes 10 minutes
✅ Fast unit tests by default, slow tests tagged and optional
Don't: Over-mock everything
❌ Mock database with in-memory fake that doesn't match real DB behavior
✅ Use real SQLite in-memory or temp files for integration tests
Don't: Hardcode test data in tests
❌ Every test creates new Project { Name = "Test", VideoPath = "test.mp4", ... }
✅ Use test data builders: TestData.CreateProject()
When This Skill Applies
Use this multi-tiered strategy when your application has:
- External API dependencies (especially paid APIs)
- Large file processing (video, audio, images)
- ML model inference (local or cloud)
- Third-party service integrations (payment, social media)
- Long-running operations (>5 seconds)
Indicators you need this approach:
- "Our tests cost $50/month to run"
- "Tests take 30 minutes on CI"
- "Tests fail randomly due to API rate limits"
- "Can't run tests without internet/API keys"
- "Downloaded 10GB of models just to run tests"