{"id":49281868,"url":"https://github.com/oatsoda/teepee","last_synced_at":"2026-04-25T19:01:47.316Z","repository":{"id":37855157,"uuid":"300858918","full_name":"oatsoda/TeePee","owner":"oatsoda","description":"A fluent API to configure HttpClients for unit testing.","archived":false,"fork":false,"pushed_at":"2026-04-25T17:29:59.000Z","size":323,"stargazers_count":4,"open_issues_count":3,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-25T18:09:26.358Z","etag":null,"topics":["httpclient","injection","mocking","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oatsoda.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-10-03T10:58:32.000Z","updated_at":"2026-04-25T17:28:38.000Z","dependencies_parsed_at":"2025-07-19T09:05:11.329Z","dependency_job_id":"c5c1c1a6-914e-439f-9f7a-e85341957325","html_url":"https://github.com/oatsoda/TeePee","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/oatsoda/TeePee","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oatsoda%2FTeePee","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oatsoda%2FTeePee/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oatsoda%2FTeePee/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oatsoda%2FTeePee/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oatsoda","download_url":"https://codeload.github.com/oatsoda/TeePee/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oatsoda%2FTeePee/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32273223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["httpclient","injection","mocking","unit-testing"],"created_at":"2026-04-25T19:01:45.114Z","updated_at":"2026-04-25T19:01:47.308Z","avatar_url":"https://github.com/oatsoda.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://raw.githubusercontent.com/oatsoda/TeePee/main/teepee-icon.png\" alt=\"TeePee Logo\" width=\"64\" height=\"64\" /\u003e\n\n# TeePee\n\nNuGet: [![NuGet Downloads](https://img.shields.io/nuget/dt/TeePee?label=NuGet%20Downloads)](https://www.nuget.org/packages/TeePee/)\n\nA fluent API to configure HttpClients for unit testing.\n\n[![Build Status](https://dev.azure.com/oatsoda/TeePee/_apis/build/status/TeePee?branchName=main)](https://dev.azure.com/oatsoda/TeePee/_build/latest?definitionId=5\u0026branchName=main)\n\n# TeePee.Refit\n\nAlso, [TeePee.Refit](/TeePee.Refit/) an add on adaptor when production code is using Refit.\n\n# Documentation\n\n## Mocking\n\nEverything in TeePee starts by creating a `TeePeeBuilder`.\n\n```csharp\nvar teePeeBuilder = new TeePeeBuilder();\n```\n\n### Matching requests\n\nAdd requests that you want to support by using the fluent API to specify as little or as much you want to match on:\n\n```csharp\nteePeeBuilder.ForRequest(\"https://some.api/path/resource\", HttpMethod.Post)\n             .ThatHasBody(new { Value = 12 })\n             .ThatContainsQueryParam(\"filter\", \"those\")\n             .ThatContainsHeader(\"ApiKey\", \"123abc-xyz987\");\n```\n\n#### Query strings\n\nQuery strings can either be included in the URL:\n\n```csharp\nteePeeBuilder.ForRequest(\"https://some.api/path/resource?filter=those\")\n```\n\nor by matching using the `ContainsQueryParam`\n\n```csharp\nteePeeBuilder.ForRequest(\"https://some.api/path/resource\", HttpMethod.Post)\n             .ThatContainsQueryParam(\"filter\", \"those\")\n```\n\nYou cannot combine both though. 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`.\n\n### Returning responses\n\nThe response to a matching request is set using the `Responds()` fluent method:\n\n```csharp\nteePeeBuilder.ForRequest(\"https://some.api/path/resource\", HttpMethod.Post)\n             .Responds()\n             .WithStatus(HttpStatusCode.OK)\n             .WithBody(new { Result = \"Done\" })\n             .WithHeader(\"Set-Cookie\", \"Yum\");\n```\n\n#### Defaults\n\nIf 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)\nIf 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)\n\n### Defaults for no matches \u0026 Strict Mode\n\nIf 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`)\n\n## Unit Testing\n\nIt's worth [making sure you fully understand](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#consumption-patterns) the various `HttpClientFactory` mechanisms for registering and resolving `HttpClient`s before reading this.\n\n### Classicist/Detroit/Black Box vs Mockist/London/White Box\n\nTeePee 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.\n\n### Verifying\n\nWhen 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.\n\nThis 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.\n\nIn this case, you can set up a tracker using `TrackRequest()` to make simple verification of the requests that you set up.\n\n```csharp\nvar requestTracker = teePeeBuilder.ForRequest(\"https://some.api/path/resource\", HttpMethod.Post)\n                                  .TrackRequets();\n\n// Execute SUT\n\nreququestTracker.WasCalled(1);\n```\n\n### Injection during Unit Tests\n\nAs 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.\n\n### Manual Injection\n\nOnce 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:\n\n#### Basic HttpClient\n\nBasic 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.\n\n```csharp\nvar teePee = await teePeeBuilder.Build();\nvar httpClientFactory = teePee.Manual().CreateHttpClientFactory();\nvar subjectUnderTest = new UserController(httpClientFactory);\n```\n\n#### Named HttpClient\n\nFor Named HttpClient instances, you need to specify the expected Name of the instance when creating the `TeePeeBuilder`:\n\n```csharp\nvar teePeeBuilder = new TeePeeBuilder(\"GitHub\");\n\n// Setup request matching...\n\nvar teePee = await teePeeBuilder.Build();\nvar httpClientFactory = teePee.Manual().CreateHttpClientFactory();\nvar subjectUnderTest = new UserController(httpClientFactory);\n```\n\n#### Typed HttpClient\n\nFor Typed HttpClient instances, you need to create the HttpClient instead of the HttpClientFactory:\n\n```csharp\nvar teePeeBuilder = new TeePeeBuilder();\n\n// Setup request matching...\n\nvar teePee = await teePeeBuilder.Build();\nvar typedHttpClient = new MyTypedHttpClient(teePee.Manual().CreateClient());\nvar subjectUnderTest = new UserController(typedHttpClient);\n```\n\n#### HttpClient BaseAddress\n\nIf you are wanting to specify the `BaseAddress` in your `HttpClient` and use Relative URLs in your Subject Under Test when calling the HttpClient, you can set TeePee up to ALSO configure this in your tests. (Note, this obviously means you are not covering this in your tests, it is just so that the HttpClient accepts Relative URLs.\n\nTo do this, pass a dummy Base Address into the `Manual()` call.\n\n```csharp\nvar teePeeBuilder = new TeePeeBuilder(\"GitHub\");\nteePeeBuilder.ForRequest(\"https://some.api/path/resource\", HttpMethod.Get)\n             .Responds()\n             .WithStatusCode(HttpStatusCode.OK);\n\nvar teePee = await teePeeBuilder.Build();\nvar typedHttpClient = new MyTypedHttpClient(teePee.Manual(\"https://some.api\").CreateClient());\nvar subjectUnderTest = new UserController(typedHttpClient);\n```\n\n### Auto Injection\n\nInjecting automatically allows you to cover the startup DI registrations as part of your unit tests. This is mostly done using the `Resolve` static class.\n\n#### Basic HttpClient\n\nBasic 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.\n\n```csharp\nvar subjectUnderTest = await Resolve.WithDefaultClient\u003cUserController\u003e(teePeeBuilder);\n```\n\n#### Named HttpClient\n\nFor Named HttpClient instances, you need to specify the expected Name of the instance when creating the `TeePeeBuilder`:\n\n```csharp\nvar teePeeBuilder = new TeePeeBuilder(\"GitHub\");\n\n// Setup request matching...\n\nvar subjectUnderTest = await Resolve.WithNamedClients\u003cUserController\u003e(\n                                services =\u003e\n                                {\n                                   // Call your production code/extension methods here - but for this example we're inlining it - see examples for further details\n                                   // Expect any intermediate dependencies to also be registered\n                                   services.AddHttpClient(\"GitHub, c =\u003e c.BaseAddress = \"https://external.api\");\n                                },\n                                teePeeBuilder);\n```\n\n#### Typed HttpClient\n\nFor 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):\n\n```csharp\nvar teePeeBuilder = new TeePeeBuilder();\n\n// Setup request matching...\n\nvar subjectUnderTest = await Resolve.WithTypedClient\u003cUserController, MyTypedHttpClient\u003e(\n                                services =\u003e\n                                {\n                                   // Call your production code/extension methods here - but for this example we're inlining it - see examples for further details\n                                   // Expect any intermediate dependencies to also be registered\n                                   services.AddHttpClient\u003cMyTypedHttpClient\u003e(c =\u003e c.BaseAddress = \"https://external.api\");\n                                },\n                                teePeeBuilder);\n```\n\n## Multiple HttpClient dependencies\n\nSee [Examples](https://github.com/oatsoda/TeePee/tree/main/TeePee/Examples) for demonstrations of how to apply the above Manual or Auto injection when you have multiple HttpClient dependencies.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foatsoda%2Fteepee","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foatsoda%2Fteepee","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foatsoda%2Fteepee/lists"}