TeePee 5.0.1
dotnet add package TeePee --version 5.0.1
NuGet\Install-Package TeePee -Version 5.0.1
<PackageReference Include="TeePee" Version="5.0.1" />
<PackageVersion Include="TeePee" Version="5.0.1" />
<PackageReference Include="TeePee" />
paket add TeePee --version 5.0.1
#r "nuget: TeePee, 5.0.1"
#:package TeePee@5.0.1
#addin nuget:?package=TeePee&version=5.0.1
#tool nuget:?package=TeePee&version=5.0.1
<img src="https://raw.githubusercontent.com/oatsoda/TeePee/main/teepee-icon.png" alt="TeePee Logo" width="64" height="64" />
TeePee
A fluent API to configure HttpClients for unit testing.
TeePee.Refit
Also, TeePee.Refit an add on adaptor when production code is using Refit.
Documentation
Mocking
Everything in TeePee starts by creating a TeePeeBuilder.
var teePeeBuilder = new TeePeeBuilder();
Matching requests
Add requests that you want to support by using the fluent API to specify as little or as much you want to match on:
teePeeBuilder
.ForRequest("https://some.api/path/resource", HttpMethod.Post)
.ThatHasBody(new { Value = 12 })
.ThatContainsQueryParam("filter", "those")
.ThatContainsHeader("ApiKey", "123abc-xyz987");
Query strings
Query strings can either be included in the URL:
teePeeBuilder.ForRequest("https://some.api/path/resource?filter=those")
or by matching using the ContainsQueryParam
teePeeBuilder
.ForRequest("https://some.api/path/resource", HttpMethod.Post)
.ThatContainsQueryParam("filter", "those")
You cannot combine both. Once you specify ContainingQueryParam then incoming requests at execution-time will have their query string removed when attempting to match a rule which is using ContainingQueryParam.
Returning responses
The response to a matching request is set using the Responds() fluent method:
teePeeBuilder
.ForRequest("https://some.api/path/resource", HttpMethod.Post)
.Responds()
.WithStatus(HttpStatusCode.OK)
.WithBody(new { Result = "Done" })
.WithHeader("Set-Cookie", "Yum");
Defaults
If you don't specify a Status Code in the response, the default is 204 NoContent. (i.e. it matched, but you didn't tell it what status to return)
If you don't call Responds() then the default response Status Code is 202 Accepted. (i.e. it matched, but you didn't tell it to respond)
Defaults for no matches & Strict Mode
If there is no match for a request, the default is to response with a Status Code is 404 NotFound. (This is configurable using the WithDefaultResponse on TeePeeBuilder)
Unit Testing
It's worth making sure you fully understand the various HttpClientFactory mechanisms for registering and resolving HttpClients before reading this.
Classicist/Detroit/Black Box vs Mockist/London/White Box
TeePee is focused on the Classicist/Detroit/Black Box approach to unit testing. It can be used for Mockist/London/White Box approaches, but be aware that due to the way HttpClientFactory is implemented, you may find there are limitations if you are planning to mock and inject your dependencies into your test subject manually.
Verifying
When Black Box unit testing, it's recommended to be as passive with mocked dependencies as possible. This means, where possible, not asserting specific details about calls to the HttpClient but instead mocking the requests and responses and instead asserting the outcomes of the Subject Under Test.
This isn't always possible - for example in a Fire and Forget situation where the behaviour is that the Subject Under Test is required to call an external HTTP service, but the SUT itself doesn't indicate this was done correctly.
In this case, you can set up a tracker using TrackRequest() to make simple verification of the requests that you set up.
var requestTracker = teePeeBuilder
.ForRequest("https://some.api/path/resource", HttpMethod.Post)
.TrackRequets();
// Execute SUT
reququestTracker.WasCalled(1);
Injection during Unit Tests
As stated above, TeePee is more focused on the Classicist/Detroit/Black Box of testing approach and this allows unit test coverage of DI registrations for HttpClientFactory. You can of course still manually inject should you wish to.
Manual Injection
Once you have finished setting up one or more requests in you TeePeeBuilder then depending on your HttpClientFactory approach, you can create the relevant objects to inject:
Basic HttpClient
Basic HttpClient usage is very limited and is only really meant for intermediate refactoring stages. You probably won't want to use this in your production code.
var httpClientFactory = teePeeBuilder.Manual().CreateHttpClientFactory(""); // Empty string is the name of the Default Client; Use Options.DefaultName if you prefer.
var subjectUnderTest = new UserController(httpClientFactory);
Named HttpClient
For Named HttpClient instances, pass the expected name of the named client:
var httpClientFactory = teePeeBuilder.Manual().CreateHttpClientFactory("GitHub");
var subjectUnderTest = new UserController(httpClientFactory);
Typed HttpClient
For Typed HttpClient instances, you need to create the HttpClient instead of the HttpClientFactory:
var typedHttpClient = new MyTypedHttpClient(teePeeBuilder.Manual().CreateClient());
var subjectUnderTest = new UserController(typedHttpClient);
HttpClient BaseAddress
If you are using manual injection and your SUT uses relative URLs, then the base URL isn't actually under test. You can either use relative URLs when building your expected rules, or if you use an arbitrary base address, then you'll need to also configure this on the manually created HttpClient or HttpClientFactory.
To do this, pass a dummy Base Address into the Manual() call.
var teePeeBuilder = new TeePeeBuilder();
teePeeBuilder
.ForRequest("https://some.api/path/resource", HttpMethod.Get)
.Responds()
.WithStatusCode(HttpStatusCode.OK);
var typedHttpClient = new MyTypedHttpClient(teePeeBuilder.Manual("https://some.api").CreateClient());
var subjectUnderTest = new UserController(typedHttpClient);
Auto Injection
Injecting automatically allows you to cover the startup DI registrations as part of your unit tests, but also lends itself to better testing of expected behaviours rather than specific implementation details (aside from Typed-clients; see below).
Auto injection using TeePee assumes that you are creating a ServiceCollection for tests and calling the production registrations, and then attaching the TeePee rules to that.
Basic HttpClient
Basic HttpClient usage is very limited and is only really meant for intermediate refactoring stages. You probably won't want to use this in your production code.
var serviceCollection = new ServiceCollection()
.AddProductionRegistrations()
.AttachToDefaultClient(teePeeBuilder);
var subjectUnderTest = serviceCollection.BuildServiceProvider().GetRequiredService<UserController>();
Named HttpClient
For Named HttpClient instances, you need to specify the expected Name of the instance:
var serviceCollection = new ServiceCollection()
.AddProductionRegistrations()
.AttachToNamedClient(teePeeBuilder, "GitHub");
Typed HttpClient
For Typed HttpClients, your unit tests unfortunately will need to know which Type is the HttpClient (therefore exposing a bit of internal implementation detail into your tests):
var serviceCollection = new ServiceCollection()
.AddProductionRegistrations()
.AttachToTypedClient<MyTypedHttpClient>(teePeeBuilder, "GitHub");
Multiple HttpClient dependencies
See Examples for demonstrations of how to apply the above Manual or Auto injection when you have multiple HttpClient dependencies.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Microsoft.Extensions.Http (>= 10.0.7)
- System.Net.Http.Json (>= 10.0.7)
- System.Text.Json (>= 10.0.7)
-
net10.0
- Microsoft.Extensions.Http (>= 10.0.7)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on TeePee:
| Package | Downloads |
|---|---|
|
TeePee.Refit
Refit extensions for TeePee. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.0.1 | 134 | 7/5/2026 |
| 4.1.2 | 197 | 4/25/2026 |
| 4.0.3 | 194 | 1/16/2026 |
| 4.0.1 | 144 | 1/15/2026 |
| 3.0.7 | 355 | 8/30/2025 |
| 3.0.6 | 326 | 8/30/2025 |
| 3.0.5 | 297 | 8/30/2025 |
| 3.0.4 | 281 | 8/17/2025 |
| 3.0.3 | 287 | 7/13/2025 |
| 3.0.2 | 390 | 3/30/2025 |
| 3.0.1 | 369 | 3/23/2025 |
| 2.0.1 | 584 | 8/15/2023 |
| 1.2.1 | 398 | 8/14/2023 |
| 1.1.1 | 455 | 8/9/2023 |
| 1.0.40 | 524 | 7/27/2023 |
| 1.0.39 | 344 | 7/27/2023 |
| 1.0.38 | 1,255 | 8/3/2022 |
| 1.0.37 | 1,198 | 8/2/2022 |
| 1.0.36 | 956 | 7/19/2022 |
| 1.0.34 | 1,006 | 6/15/2022 |