├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── pull_request.yml ├── .gitignore ├── ChangeLog.md ├── HttpClient.Helpers.sln ├── LICENSE.md ├── README.md ├── appveyor.yml ├── coverlet.runsettings ├── global.json ├── icon.jpg ├── src └── HttpClient.Helpers │ ├── FakeMessageHandler.cs │ ├── HttpClient.Helpers.csproj │ └── HttpMessageOptions.cs └── tests └── HttpClient.Helpers.Tests ├── GetAsyncTests.cs ├── GetStringAsyncTests.cs ├── HttpClient.Helpers.Tests.csproj ├── PostAsyncTests.cs ├── PutAsyncTests.cs └── Wiki Examples ├── Baa.cs ├── Foo.cs ├── MultipleEndPointTests.cs ├── MyService.cs └── SingleEndpointTests.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | 5 | #### How to manually clean up code in an IDE? 6 | 7 | #### - Visual Studio: CTRL + K + D 8 | #### - VSCode: SHIFT + ALT + F 9 | 10 | 11 | 12 | #################################################################################################### 13 | 14 | ### VS Settings Reference: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 15 | 16 | 17 | 18 | 19 | # top-most EditorConfig file 20 | root = true 21 | 22 | # Default settings: 23 | # A newline ending every file 24 | # Use 4 spaces as indentation 25 | [*] 26 | insert_final_newline = true 27 | indent_style = space 28 | indent_size = 4 29 | end_of_line = crlf 30 | charset = utf-8 31 | 32 | # C# files 33 | [*.cs] 34 | # New line preferences 35 | csharp_new_line_before_open_brace = all 36 | csharp_new_line_before_else = true 37 | csharp_new_line_before_catch = true 38 | csharp_new_line_before_finally = true 39 | csharp_new_line_before_members_in_object_initializers = true 40 | csharp_new_line_before_members_in_anonymous_types = true 41 | csharp_new_line_between_query_expression_clauses = true 42 | 43 | # Indentation preferences 44 | csharp_indent_block_contents = true 45 | csharp_indent_braces = false 46 | csharp_indent_case_contents = true 47 | csharp_indent_switch_labels = true 48 | csharp_indent_labels = one_less_than_current 49 | 50 | # avoid this. unless absolutely necessary 51 | dotnet_style_qualification_for_field = false:suggestion 52 | dotnet_style_qualification_for_property = false:suggestion 53 | dotnet_style_qualification_for_method = false:suggestion 54 | dotnet_style_qualification_for_event = false:suggestion 55 | 56 | # only use var when it's obvious what the variable type is 57 | csharp_style_var_for_built_in_types = true:suggestion 58 | csharp_style_var_when_type_is_apparent = true:suggestion 59 | csharp_style_var_elsewhere = true:suggestion 60 | 61 | # use language keywords instead of BCL types 62 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 63 | dotnet_style_predefined_type_for_member_access = true:suggestion 64 | 65 | # name all constant fields using PascalCase 66 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 67 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 68 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 69 | 70 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 71 | dotnet_naming_symbols.constant_fields.required_modifiers = const 72 | 73 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 74 | 75 | # static fields should have s_ prefix 76 | dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion 77 | dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields 78 | dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style 79 | 80 | dotnet_naming_symbols.static_fields.applicable_kinds = field 81 | dotnet_naming_symbols.static_fields.required_modifiers = static 82 | 83 | dotnet_naming_style.static_prefix_style.required_prefix = s_ 84 | dotnet_naming_style.static_prefix_style.capitalization = camel_case 85 | 86 | # internal and private fields should be _camelCase 87 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 88 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 89 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 90 | 91 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field 92 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal 93 | 94 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 95 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 96 | 97 | # Code style defaults 98 | dotnet_sort_system_directives_first = true 99 | csharp_preserve_single_line_blocks = true 100 | csharp_preserve_single_line_statements = false 101 | 102 | # Expression-level preferences 103 | dotnet_style_object_initializer = true:suggestion 104 | dotnet_style_collection_initializer = true:suggestion 105 | dotnet_style_explicit_tuple_names = true:suggestion 106 | dotnet_style_coalesce_expression = true:suggestion 107 | dotnet_style_null_propagation = true:suggestion 108 | 109 | # Expression-bodied members 110 | csharp_style_expression_bodied_methods = false:none 111 | csharp_style_expression_bodied_constructors = false:none 112 | csharp_style_expression_bodied_operators = false:none 113 | csharp_style_expression_bodied_properties = true:none 114 | csharp_style_expression_bodied_indexers = true:none 115 | csharp_style_expression_bodied_accessors = true:none 116 | 117 | # Pattern matching 118 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 119 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 120 | csharp_style_inlined_variable_declaration = true:suggestion 121 | 122 | # Null checking preferences 123 | csharp_style_throw_expression = true:suggestion 124 | csharp_style_conditional_delegate_call = true:suggestion 125 | 126 | # Space preferences 127 | csharp_space_after_cast = false 128 | csharp_space_after_colon_in_inheritance_clause = true 129 | csharp_space_after_comma = true 130 | csharp_space_after_dot = false 131 | csharp_space_after_keywords_in_control_flow_statements = true 132 | csharp_space_after_semicolon_in_for_statement = true 133 | csharp_space_around_binary_operators = before_and_after 134 | csharp_space_around_declaration_statements = do_not_ignore 135 | csharp_space_before_colon_in_inheritance_clause = true 136 | csharp_space_before_comma = false 137 | csharp_space_before_dot = false 138 | csharp_space_before_open_square_brackets = false 139 | csharp_space_before_semicolon_in_for_statement = false 140 | csharp_space_between_empty_square_brackets = false 141 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 142 | csharp_space_between_method_call_name_and_opening_parenthesis = false 143 | csharp_space_between_method_call_parameter_list_parentheses = false 144 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 145 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 146 | csharp_space_between_method_declaration_parameter_list_parentheses = false 147 | csharp_space_between_parentheses = false 148 | csharp_space_between_square_brackets = false 149 | 150 | [*.{asm,inc}] 151 | indent_size = 8 152 | 153 | # Xml project files 154 | [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 155 | indent_size = 2 156 | 157 | # Xml config files 158 | [*.{props,targets,config,nuspec}] 159 | indent_size = 2 160 | 161 | [CMakeLists.txt] 162 | indent_size = 2 163 | 164 | [*.cmd] 165 | indent_size = 2 166 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: PureKrome 2 | custom: ['https://paypal.me/purekrome'] 3 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.obj 2 | *.pdb 3 | *.user 4 | *.aps 5 | *.pch 6 | *.vspscc 7 | *.vssscc 8 | *_i.c 9 | *_p.c 10 | *.ncb 11 | *.suo 12 | *.tlb 13 | *.tlh 14 | *.bak 15 | *.cache 16 | *.ilk 17 | *.log 18 | *.lib 19 | *.sbr 20 | *.scc 21 | [Bb]in 22 | [Dd]ebug*/ 23 | obj/ 24 | [Rr]elease*/ 25 | _ReSharper*/ 26 | *.[Pp]ublish.xml 27 | *.resharper* 28 | AppData/ 29 | App_Data/ 30 | *.log.* 31 | [Ll]ogs/ 32 | [Pp]ackages/ 33 | [Tt]humbs.db 34 | [Tt]est[Rr]esult* 35 | [Bb]uild[Ll]og.* 36 | *.sln.DotSettings.* 37 | *.ncrunchproject 38 | *.ncrunchsolution 39 | *.nupkg 40 | .vs 41 | *.orig -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [7.1.0] - 2019-07-08 7 | ### Fixed 8 | - NumberOfTimesCount is now thread-safe. 9 | 10 | ## [7.1.0] - 2019-07-08 11 | ### Added 12 | - Added GitHub sourcelink integration. 13 | 14 | ## [7.0.0] - 2019-05-28 15 | ### Changed 16 | - Updated to .NET Standard 2.0. 17 | - Better error message when failed to match the setup Options. 18 | 19 | ## [6.1.0] - 2017-06-07 20 | ### Fixed 21 | - The new .NET Standard packaging (in AppVeyor) was creating an incorrect Assembly name. :blush: 22 | 23 | ## [6.0.0] - 2017-05-27 24 | ### Changed 25 | - Supports .NET Standard 1.4! Woot! 26 | 27 | ### Fixed 28 | - #27: Uri fails to compare/equals when Uri contains characters that get encoded. 29 | 30 | ## [5.1.0] - 2017-02-06 31 | ### Added 32 | - Support for `Headers` in `HttpMessageOptions`. 33 | 34 | 35 | ## [1.0.0 -> 5.0.0] - 2014-08-07 -> 2017-02-06 36 | ### Added 37 | - Inital and subsequent releases in supporting faking an `HttpClient` request/response. 38 | -------------------------------------------------------------------------------- /HttpClient.Helpers.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpClient.Helpers", "src\HttpClient.Helpers\HttpClient.Helpers.csproj", "{E3CE6793-DBBF-4E27-AC2B-D680C4D90300}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpClient.Helpers.Tests", "tests\HttpClient.Helpers.Tests\HttpClient.Helpers.Tests.csproj", "{5FAB6B8D-A203-41A8-BDC2-FA4C95DC7E4A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {E3CE6793-DBBF-4E27-AC2B-D680C4D90300}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {E3CE6793-DBBF-4E27-AC2B-D680C4D90300}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {E3CE6793-DBBF-4E27-AC2B-D680C4D90300}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {E3CE6793-DBBF-4E27-AC2B-D680C4D90300}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {5FAB6B8D-A203-41A8-BDC2-FA4C95DC7E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5FAB6B8D-A203-41A8-BDC2-FA4C95DC7E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5FAB6B8D-A203-41A8-BDC2-FA4C95DC7E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5FAB6B8D-A203-41A8-BDC2-FA4C95DC7E4A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Justin Adler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HttpClient.Helpers 2 | 3 | ![Lic: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square) 4 | 5 | | Stage | CI | NuGet | 6 | |-------------|----|-------| 7 | | Production | [![Build status](https://ci.appveyor.com/api/projects/status/siwilxb8t3enyus2/branch/master?svg=true)](https://ci.appveyor.com/project/PureKrome/httpclient-helpers) | [![NuGet Badge](https://buildstats.info/nuget/WorldDomination.HttpClient.Helpers)](https://www.nuget.org/packages/WorldDomination.HttpClient.Helpers/) | 8 | | Development | [![Build status](https://ci.appveyor.com/api/projects/status/siwilxb8t3enyus2/branch/dev?svg=true)](https://ci.appveyor.com/project/PureKrome/httpclient-helpers) | [![MyGet Badge](https://buildstats.info/myget/pk-development/WorldDomination.HttpClient.Helpers)](https://www.myget.org/feed/pk-development/package/nuget/WorldDomination.HttpClient.Helpers) | 9 | 10 | --- 11 | 12 | Code that uses `System.Net.Http.HttpClient` will attempt to actually call/hit that Http endpoint. 13 | 14 | To prevent this from happening in a *unit* test, some simple helpers are provided in this code library. 15 | 16 | ## Key Points 17 | - :white_check_mark: Hijack your `httpClient` request to return some hardcoded response (this library makes it _supa dupa easy_ to do this) 18 | - :white_check_mark: Works with GET/POST/PUT/etc. 19 | - :white_check_mark: Can provide wildcards (i.e. I don't care about the Request endpoint or the request HTTP Method, etc) 20 | - :white_check_mark: Can provide multiple endpoints and see handle what is returned based on the particular request. 21 | - :white_check_mark: Can confirm the number of times an endpoint was attempted to be hit. 22 | - :white_check_mark: Can be used to test network errors during transmission. i.e. can test when the HttpClient throws an exception because of .. well ... :boom: 23 | ----- 24 | 25 | ## Installation 26 | 27 | [![](https://i.imgur.com/oLtAwq9.png)](https://www.nuget.org/packages/WorldDomination.HttpClient.Helpers/) 28 | 29 | Package Name: `WorldDomination.HttpClient.Helpers` 30 | CLI: `install-package WorldDomination.HttpClient.Helpers` 31 | 32 | ## TL;DR; Show me some code that leverages HttpClientFactory 33 | 34 | What is `HttpClientFactory`? [Read up about it here](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests). You should be using that in your applications, peeps. 35 | ```C# 36 | // Service that accepts an HttpClient so it has been setup to work nicely with HttpClientFactory. 37 | public class MyService : IMyService 38 | { 39 | public MyService(HttpClient httpClient) 40 | { .. } 41 | 42 | public async Task GetWebApiStuffAsync() { .. } 43 | } 44 | 45 | // ** Now for a sample test ** 46 | 47 | // Act. 48 | 49 | // Setup which http request 'stuff' you wish to capture and return. 50 | // E.g. If we hit "this url" then return "this payload" and "this response type. 200 OK, etc". 51 | var options = new HttpMessageOptions { ....}; 52 | 53 | // Create the fake handler with the specific options to check for. 54 | var messageHandler = new FakeHttpMessageHandler(options); 55 | 56 | // Now create a simple HttpClient which will use the fake handler to 57 | // cut short / capture the request. 58 | var httpClient = new HttpClient(messageHandler); 59 | 60 | var service = new MyService(httpClient); 61 | 62 | // Act. 63 | await service.GetWebApiStuffAsync(); 64 | ``` 65 | 66 | ## Sample Code 67 | 68 | There's [plenty more examples](https://github.com/PureKrome/HttpClient.Helpers/wiki) about to wire up: 69 | - [A really simple example](https://github.com/PureKrome/HttpClient.Helpers/wiki/A-single-endpoint) 70 | - [Multiple endpoints](https://github.com/PureKrome/HttpClient.Helpers/wiki/Multiple-endpoints) at once 71 | - [Wildcard endpoints](https://github.com/PureKrome/HttpClient.Helpers/wiki/Wildcard-endpoints) 72 | - [Throwing exceptions](https://github.com/PureKrome/HttpClient.Helpers/wiki/Faking-an-Exception) and handling it 73 | 74 | For all the samples, please [check out the Wiki page: Helper Examples](https://github.com/PureKrome/HttpClient.Helpers/wiki) 75 | 76 | ----- 77 | 78 | Special Ackowledgements 79 | 80 | A special and sincere *thank you* to David Fowler ([@davidfowl](https://www.twitter.com/davidfowl)) who explained how I should be unit testing the `HttpClient` class and gave me the guidence to make this library. I was soooo on the wrong path - and he guided me back on track. 81 | 82 | Thank you David! :ok_hand: :cocktail: :space_invader: 83 | 84 | ----- 85 | 86 | ### Summary 87 | 88 | Finally, unit testing `HttpClient` is now awesome and simple! 89 | 90 | ![Wohoo](https://31.media.tumblr.com/43e63461d1e3f22a49b18dbf15227a1d/tumblr_inline_n3t10oQfIh1solpjm.gif) 91 | 92 | --- 93 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}.0.0-dev' 2 | image: Ubuntu 3 | pull_requests: 4 | do_not_increment_build_number: true 5 | skip_branch_with_pr: true 6 | 7 | # Override the 'version' if this is a GH-tag-commit -or- this is a custom branch (i.e not 'master'). 8 | init: 9 | - ps: | 10 | if ($env:APPVEYOR_REPO_TAG -eq $TRUE -and $env:APPVEYOR_REPO_BRANCH -eq 'master') 11 | { 12 | Write-Host " !! Commit is Tagged and branch is 'master' - forcing build version to tag-value." -ForegroundColor Red; 13 | Update-AppveyorBuild -Version "$env:APPVEYOR_REPO_TAG_NAME" 14 | } 15 | iex ((new-object net.webclient).DownloadString('https://gist.githubusercontent.com/PureKrome/0f79e25693d574807939/raw/f5b40256fc2ca77d49f1c7773d28406152544c1e/appveyor-build-info.ps')) 16 | 17 | 18 | matrix: 19 | fast_finish: true 20 | 21 | configuration: 22 | - Debug 23 | - Release 24 | 25 | install: 26 | - curl -sSL https://dot.net/v1/dotnet-install.sh | sudo bash /dev/stdin --channel $global_dotnet_channel --version $global_dotnet_version --install-dir /usr/share/dotnet 27 | 28 | before_build: 29 | # Display .NET Core version 30 | - dotnet --info 31 | 32 | build_script: 33 | - dotnet restore --verbosity quiet 34 | - ps: dotnet build -c $env:CONFIGURATION -v minimal --no-restore /p:ContinuousIntegrationBuild=true -p:Version=$env:APPVEYOR_BUILD_VERSION 35 | 36 | test_script: 37 | - ps: | 38 | if ($env:CONFIGURATION -eq 'Debug') 39 | { 40 | dotnet test -c $env:CONFIGURATION -v minimal --no-build --collect:"XPlat Code Coverage" --settings coverlet.runsettings --results-directory './CodeCoverageResults' 41 | } 42 | else 43 | { 44 | dotnet test -c $env:CONFIGURATION -v minimal --no-build 45 | } 46 | 47 | 48 | # We only: 49 | # - Send DEBUG results up to codecov. 50 | # - Pack for RELEASE only. 51 | after_test: 52 | - ps: | 53 | if ($env:CONFIGURATION -eq 'Debug') 54 | { 55 | # Currently, the bash script version is better than the .exe version. 56 | Invoke-WebRequest -Uri 'https://codecov.io/bash' -OutFile codecov.sh 57 | bash codecov.sh -s './CodeCoverageResults/' -f '*.xml' -Z 58 | } 59 | 60 | if ($env:CONFIGURATION -eq 'Release') 61 | { 62 | dotnet pack -c $env:CONFIGURATION --no-build -p:PackageVersion=$env:APPVEYOR_BUILD_VERSION 63 | } 64 | 65 | artifacts: 66 | - path: '**\*.nupkg' 67 | name: nuget-packages 68 | type: NuGetPackage 69 | - path: '**\*.snupkg' 70 | name: nuget-symbols 71 | type: NuGetPackage 72 | 73 | deploy: 74 | 75 | # NOTE: MyGet doesn't support snupkg's so we have to manually specify the nuget to send 76 | - provider: NuGet 77 | server: https://www.myget.org/F/pk-development/api/v2/package 78 | api_key: $(global_myget_api_key) 79 | skip_symbols: true 80 | artifact: nuget-packages 81 | on: 82 | APPVEYOR_REPO_TAG: false 83 | 84 | - provider: NuGet 85 | api_key: $(global_nuget_api_key) 86 | on: 87 | branch: master 88 | APPVEYOR_REPO_TAG: true 89 | -------------------------------------------------------------------------------- /coverlet.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | [*.Tests]* 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PureKrome/HttpClient.Helpers/2c96bcaa8285468dfce28a3e86b7f48d994c8149/icon.jpg -------------------------------------------------------------------------------- /src/HttpClient.Helpers/FakeMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace WorldDomination.Net.Http 12 | { 13 | public class FakeHttpMessageHandler : HttpClientHandler 14 | { 15 | private readonly HttpRequestException _exception; 16 | 17 | private readonly IDictionary _lotsOfOptions = new Dictionary(); 18 | 19 | /// 20 | /// A fake message handler. 21 | /// 22 | /// TIP: If you have a requestUri = "*", this is a catch-all ... so if none of the other requestUri's match, then it will fall back to this dictionary item. 23 | public FakeHttpMessageHandler(HttpMessageOptions options) : this(new List { options }) 24 | { 25 | } 26 | 27 | public FakeHttpMessageHandler(IEnumerable lotsOfOptions) 28 | { 29 | Initialize(lotsOfOptions.ToArray()); 30 | } 31 | 32 | /// 33 | /// A fake message handler for an exception. 34 | /// 35 | /// This is mainly used for unit testing exceptions when the HttpClient fails. 36 | /// The exception that will occur. 37 | public FakeHttpMessageHandler(HttpRequestException exception) 38 | { 39 | _exception = exception ?? throw new ArgumentNullException(nameof(exception)); 40 | } 41 | 42 | protected override Task SendAsync(HttpRequestMessage request, 43 | CancellationToken cancellationToken) 44 | { 45 | if (_exception != null) 46 | { 47 | throw _exception; 48 | } 49 | 50 | var tcs = new TaskCompletionSource(); 51 | 52 | var requestUri = new Uri(request.RequestUri.AbsoluteUri); 53 | var option = new HttpMessageOptions 54 | { 55 | RequestUri = requestUri, 56 | HttpMethod = request.Method, 57 | HttpContent = request.Content, 58 | Headers = request.Headers.ToDictionary(kv => kv.Key, kv => kv.Value) 59 | }; 60 | 61 | var expectedOption = GetExpectedOption(option); 62 | if (expectedOption == null) 63 | { 64 | var setupOptionsText = new StringBuilder(); 65 | 66 | var setupOptions = _lotsOfOptions.Values.ToList(); 67 | for (var i = 0; i < setupOptions.Count; i++) 68 | { 69 | if (i > 0) 70 | { 71 | setupOptionsText.Append(" "); 72 | } 73 | setupOptionsText.Append($"{i + 1}) {setupOptions[i]}."); 74 | } 75 | 76 | var errorMessage = $"No HttpResponseMessage found for the Request => What was called: [{option}]. At least one of these option(s) should have been matched: [{setupOptionsText}]"; 77 | throw new InvalidOperationException(errorMessage); 78 | } 79 | 80 | // Increment the number of times this option had been 'called'. 81 | expectedOption.IncrementNumberOfTimesCalled(); 82 | 83 | // Pass the request along. 84 | expectedOption.HttpResponseMessage.RequestMessage = request; 85 | 86 | tcs.SetResult(expectedOption.HttpResponseMessage); 87 | return tcs.Task; 88 | } 89 | 90 | /// 91 | /// Helper method to easily return a simple HttpResponseMessage. 92 | /// 93 | public static HttpResponseMessage GetStringHttpResponseMessage(string content, 94 | HttpStatusCode httpStatusCode = HttpStatusCode.OK, 95 | string mediaType = "application/json") 96 | { 97 | return new HttpResponseMessage 98 | { 99 | StatusCode = httpStatusCode, 100 | Content = new StringContent(content, Encoding.UTF8, mediaType) 101 | }; 102 | } 103 | 104 | private void Initialize(HttpMessageOptions[] lotsOfOptions) 105 | { 106 | if (lotsOfOptions == null) 107 | { 108 | throw new ArgumentNullException(nameof(lotsOfOptions)); 109 | } 110 | 111 | if (!lotsOfOptions.Any()) 112 | { 113 | throw new ArgumentOutOfRangeException(nameof(lotsOfOptions), 114 | "Need at least _one_ expected request/response (a.k.a. HttpMessageOptions) setup."); 115 | } 116 | 117 | // We need to make sure the requests are unique. 118 | foreach (var option in lotsOfOptions) 119 | { 120 | if (_lotsOfOptions.ContainsKey(option.ToString())) 121 | { 122 | throw new InvalidOperationException( 123 | $"Trying to add a request/response (a.k.a. HttpMessageOptions) which has already been setup. Can only have one unique request/response, setup. Unique info: {option}"); 124 | } 125 | 126 | _lotsOfOptions.Add(option.ToString(), option); 127 | } 128 | } 129 | 130 | private HttpMessageOptions GetExpectedOption(HttpMessageOptions option) 131 | { 132 | if (option == null) 133 | { 134 | throw new ArgumentNullException(nameof(option)); 135 | } 136 | 137 | // NOTE: We only compare the *setup* HttpMessageOptions properties if they were provided. 138 | // So if there was no HEADERS provided ... but the real 'option' has some, we still ignore 139 | // and don't compare. 140 | return _lotsOfOptions.Values.SingleOrDefault(x => (x.RequestUri == null || // Don't care about the Request Uri. 141 | x.RequestUri.AbsoluteUri.Equals(option.RequestUri.AbsoluteUri, StringComparison.OrdinalIgnoreCase)) && 142 | 143 | (x.HttpMethod == null || // Don't care about the HttpMethod. 144 | x.HttpMethod == option.HttpMethod) && 145 | 146 | (x.HttpContent == null || // Don't care about the Content. 147 | ContentAreEqual(x.HttpContent, option.HttpContent)) && 148 | 149 | (x.Headers == null || // Don't care about the Header. 150 | x.Headers.Count == 0 || // No header's were supplied, so again don't care/ 151 | HeadersAreEqual(x.Headers, option.Headers))); 152 | } 153 | 154 | private static bool ContentAreEqual(HttpContent source, HttpContent destination) 155 | { 156 | if (source == null && 157 | destination == null) 158 | { 159 | // Both are null - so they match :P 160 | return true; 161 | } 162 | 163 | if (source == null || 164 | destination == null) 165 | { 166 | return false; 167 | } 168 | 169 | // Extract the content from both HttpContent's. 170 | var sourceContentTask = source.ReadAsStringAsync(); 171 | var destinationContentTask = destination.ReadAsStringAsync(); 172 | var tasks = new List 173 | { 174 | sourceContentTask, 175 | destinationContentTask 176 | }; 177 | Task.WaitAll(tasks.ToArray()); 178 | 179 | // Now compare both results. 180 | // NOTE: Case sensitive. 181 | return sourceContentTask.Result == destinationContentTask.Result; 182 | } 183 | 184 | private static bool HeadersAreEqual(IDictionary> source, 185 | IDictionary> destination) 186 | { 187 | if (source == null && 188 | destination == null) 189 | { 190 | // Nothing from both .. so that's ok! 191 | return true; 192 | } 193 | 194 | if (source == null || 195 | destination == null) 196 | { 197 | // At least one is different so don't bother checking. 198 | return false; 199 | } 200 | 201 | // Both sides are not the same size. 202 | if (source.Count != destination.Count) 203 | { 204 | return false; 205 | } 206 | 207 | foreach (var key in source.Keys) 208 | { 209 | if (!destination.ContainsKey(key)) 210 | { 211 | // Key is missing from the destination. 212 | return false; 213 | } 214 | 215 | if (source[key].Count() != destination[key].Count()) 216 | { 217 | // The destination now doesn't have the same size of 'values'. 218 | return false; 219 | } 220 | 221 | foreach (var value in source[key]) 222 | { 223 | if (!destination[key].Contains(value)) 224 | { 225 | return false; 226 | } 227 | } 228 | } 229 | 230 | return true; 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/HttpClient.Helpers/HttpClient.Helpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net5.0 5 | Some simple System.Net.Http.HttpClient helpers to help with your unit tests or when you don't really want to call the endpoint. 6 | False 7 | World-Domination Technologies Pty. Ltd. 8 | Justin Adler and awesome contributors. 9 | WorldDomination.HttpClient.Helpers 10 | 0.0.0 11 | HttpClient helpers for System.Net.Http.HttpClient 12 | 2014 13 | https://github.com/PureKrome/HttpClient.Helpers 14 | https://github.com/PureKrome/HttpClient.Helpers 15 | httpclient worlddomination worldomination unicorn magicalunicorn magical-unicorn 16 | WorldDomination.Net.Http 17 | WorldDomination.HttpClient.Helpers 18 | 19 | 20 | 21 | true 22 | 23 | true 24 | snupkg 25 | LICENSE.md 26 | icon.jpg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | True 36 | 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/HttpClient.Helpers/HttpMessageOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace WorldDomination.Net.Http 9 | { 10 | public class HttpMessageOptions 11 | { 12 | private const string AnyValue = "*"; 13 | private HttpContent _httpContent; 14 | private string _httpContentSerialized; 15 | private int _numberOfTimesCalled = 0; 16 | 17 | /// 18 | /// Optional: If not provided, then assumed to be *any* endpoint. Otherise, the endpoint we are trying to call/hit/test. 19 | /// 20 | public Uri RequestUri { get; set; } 21 | 22 | /// 23 | /// Optional: If not provided, then assumed to be *any* method. 24 | /// 25 | public HttpMethod HttpMethod { get; set; } 26 | 27 | /// 28 | /// Required: Need to know what type of response we will return. 29 | /// 30 | public HttpResponseMessage HttpResponseMessage { get; set; } 31 | 32 | /// 33 | /// Optional: If not provided, then assumed to be *no* content. 34 | /// 35 | public HttpContent HttpContent 36 | { 37 | get => _httpContent; 38 | set 39 | { 40 | _httpContent = value; 41 | _httpContentSerialized = _httpContent == null 42 | ? null 43 | : Task.Run(_httpContent.ReadAsStringAsync).Result; 44 | } 45 | } 46 | 47 | /// 48 | /// Optional: If not provided, then assumed to have *no* headers. 49 | /// 50 | public IDictionary> Headers { get; set; } 51 | 52 | // Note: I'm using reflection to set the value in here because I want this value to be _read-only_. 53 | // Secondly, this occurs during a UNIT TEST, so I consider the expensive reflection costs to be 54 | // acceptable in this situation. 55 | // ReSharper disable once UnusedAutoPropertyAccessor.Local 56 | public int NumberOfTimesCalled { get { return _numberOfTimesCalled; } } 57 | 58 | public void IncrementNumberOfTimesCalled() 59 | { 60 | Interlocked.Increment(ref _numberOfTimesCalled); 61 | } 62 | 63 | public override string ToString() 64 | { 65 | var httpMethodText = HttpMethod?.ToString() ?? AnyValue; 66 | 67 | var headers = Headers != null && 68 | Headers.Any() 69 | ? " || Headers: " + string.Join(":", Headers.Select(x => $"{x.Key}|{string.Join(",", x.Value)}")) 70 | : ""; 71 | return $"{httpMethodText} {RequestUri}{(HttpContent != null ? $" || body/content: {_httpContentSerialized}" : "")}{headers}"; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/GetAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Shouldly; 8 | using WorldDomination.Net.Http; 9 | using Xunit; 10 | 11 | // ReSharper disable ConsiderUsingConfigureAwait 12 | 13 | namespace WorldDomination.HttpClient.Helpers.Tests 14 | { 15 | public class GetAsyncTests 16 | { 17 | private static Uri RequestUri = new Uri("http://www.something.com/some/website"); 18 | private const string ExpectedContent = "pew pew"; 19 | 20 | private static List GetSomeFakeHttpMessageOptions(HttpMessageOptions option) 21 | { 22 | return new List 23 | { 24 | new HttpMessageOptions 25 | { 26 | HttpMethod = HttpMethod.Get, 27 | RequestUri = new Uri("http://some/url"), 28 | HttpResponseMessage = SomeFakeResponse 29 | }, 30 | new HttpMessageOptions 31 | { 32 | HttpMethod = HttpMethod.Get, 33 | RequestUri = new Uri("http://another/url"), 34 | HttpResponseMessage = SomeFakeResponse 35 | }, 36 | option 37 | }; 38 | } 39 | 40 | private static HttpResponseMessage SomeFakeResponse => new HttpResponseMessage(HttpStatusCode.OK) 41 | { 42 | Content = new StringContent(ExpectedContent) 43 | }; 44 | 45 | public static IEnumerable ValidHttpMessageOptions 46 | { 47 | get 48 | { 49 | yield return new object[] 50 | { 51 | // All wildcards. 52 | new HttpMessageOptions 53 | { 54 | HttpResponseMessage = SomeFakeResponse 55 | } 56 | }; 57 | 58 | // Any Uri but has to be a GET. 59 | yield return new object[] 60 | { 61 | new HttpMessageOptions 62 | { 63 | HttpMethod = HttpMethod.Get, 64 | HttpResponseMessage = SomeFakeResponse 65 | } 66 | }; 67 | 68 | // Has to match GET + URI. 69 | // NOTE: Http GET shouldn't have a content/body. 70 | yield return new object[] 71 | { 72 | new HttpMessageOptions 73 | { 74 | HttpMethod = HttpMethod.Get, 75 | RequestUri = RequestUri, 76 | HttpResponseMessage = SomeFakeResponse 77 | } 78 | }; 79 | 80 | // Has to match GET + URI + Header 81 | yield return new object[] 82 | { 83 | new HttpMessageOptions 84 | { 85 | HttpMethod = HttpMethod.Get, 86 | RequestUri = RequestUri, 87 | Headers = new Dictionary> 88 | { 89 | {"Bearer", new[] 90 | { 91 | "pewpew" 92 | } 93 | } 94 | }, 95 | HttpResponseMessage = SomeFakeResponse 96 | } 97 | }; 98 | 99 | // Has to match GET + URI + Header (but with a different case) 100 | yield return new object[] 101 | { 102 | new HttpMessageOptions 103 | { 104 | HttpMethod = HttpMethod.Get, 105 | RequestUri = RequestUri, 106 | Headers = new Dictionary> 107 | { 108 | {"Bearer", new[] 109 | { 110 | "PEWPEW" 111 | } 112 | } 113 | }, 114 | HttpResponseMessage = SomeFakeResponse 115 | } 116 | }; 117 | } 118 | } 119 | 120 | public static IEnumerable ValidSomeHttpMessageOptions 121 | { 122 | get 123 | { 124 | yield return new object[] 125 | { 126 | // All wildcards. 127 | GetSomeFakeHttpMessageOptions( 128 | new HttpMessageOptions 129 | { 130 | HttpResponseMessage = SomeFakeResponse 131 | }) 132 | }; 133 | 134 | yield return new object[] 135 | { 136 | // Any Uri but has to be a GET. 137 | GetSomeFakeHttpMessageOptions( 138 | new HttpMessageOptions 139 | { 140 | HttpMethod = HttpMethod.Get, 141 | HttpResponseMessage = SomeFakeResponse 142 | }) 143 | }; 144 | 145 | yield return new object[] 146 | { 147 | // Has to match GET + URI 148 | GetSomeFakeHttpMessageOptions( 149 | new HttpMessageOptions 150 | { 151 | HttpMethod = HttpMethod.Get, 152 | RequestUri = RequestUri, 153 | HttpResponseMessage = SomeFakeResponse 154 | }) 155 | }; 156 | 157 | yield return new object[] 158 | { 159 | // Has to match GET + URI (case sensitive) 160 | GetSomeFakeHttpMessageOptions( 161 | new HttpMessageOptions 162 | { 163 | HttpMethod = HttpMethod.Get, 164 | RequestUri = new Uri(RequestUri.AbsoluteUri.ToUpper()), 165 | HttpResponseMessage = SomeFakeResponse 166 | }) 167 | }; 168 | } 169 | } 170 | 171 | public static IEnumerable DifferentHttpMessageOptions 172 | { 173 | get 174 | { 175 | yield return new object[] 176 | { 177 | // Different uri. 178 | new HttpMessageOptions 179 | { 180 | RequestUri = new Uri("http://this.is.a.different.website") 181 | } 182 | }; 183 | 184 | yield return new object[] 185 | { 186 | // Different Method. 187 | new HttpMessageOptions 188 | { 189 | HttpMethod = HttpMethod.Head 190 | } 191 | }; 192 | 193 | yield return new object[] 194 | { 195 | // Different header (different key). 196 | new HttpMessageOptions 197 | { 198 | Headers = new Dictionary> 199 | { 200 | { 201 | "xxxx", new[] 202 | { 203 | "pewpew" 204 | } 205 | } 206 | } 207 | } 208 | }; 209 | 210 | yield return new object[] 211 | { 212 | // Different header (found key, different content). 213 | new HttpMessageOptions 214 | { 215 | Headers = new Dictionary> 216 | { 217 | { 218 | "Bearer", new[] 219 | { 220 | "pewpew" 221 | } 222 | } 223 | } 224 | } 225 | }; 226 | } 227 | } 228 | 229 | [Theory] 230 | [MemberData(nameof(ValidHttpMessageOptions))] 231 | public async Task GivenAnHttpMessageOptions_GetAsync_ReturnsAFakeResponse(HttpMessageOptions options) 232 | { 233 | // Arrange. 234 | var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); 235 | 236 | // Act. 237 | await DoGetAsync(RequestUri, 238 | ExpectedContent, 239 | fakeHttpMessageHandler, 240 | options.Headers); 241 | 242 | // Assert. 243 | options.NumberOfTimesCalled.ShouldBe(1); 244 | } 245 | 246 | [Theory] 247 | [MemberData(nameof(ValidSomeHttpMessageOptions))] 248 | public async Task GivenSomeHttpMessageOptions_GetAsync_ReturnsAFakeResponse(IList lotsOfOptions) 249 | { 250 | // Arrange. 251 | var fakeHttpMessageHandler = new FakeHttpMessageHandler(lotsOfOptions); 252 | 253 | // Act & Assert. 254 | await DoGetAsync(RequestUri, 255 | ExpectedContent, 256 | fakeHttpMessageHandler); 257 | lotsOfOptions.Sum(x => x.NumberOfTimesCalled).ShouldBe(1); 258 | } 259 | 260 | [Fact] 261 | public async Task GivenAnHttpResponseMessage_GetAsync_ReturnsAFakeResponse() 262 | { 263 | // Arrange. 264 | var httpResponseMessage = FakeHttpMessageHandler.GetStringHttpResponseMessage(ExpectedContent); 265 | var options = new HttpMessageOptions 266 | { 267 | HttpResponseMessage = httpResponseMessage 268 | }; 269 | var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); 270 | 271 | // Act & Assert. 272 | await DoGetAsync(RequestUri, 273 | ExpectedContent, 274 | fakeHttpMessageHandler); 275 | options.NumberOfTimesCalled.ShouldBe(1); 276 | } 277 | 278 | [Fact] 279 | public async Task GivenSomeHttpResponseMessages_GetAsync_ReturnsAFakeResponse() 280 | { 281 | // Arrange. 282 | var messageResponse1 = FakeHttpMessageHandler.GetStringHttpResponseMessage(ExpectedContent); 283 | 284 | const string responseData2 = "Html, I am not."; 285 | var messageResponse2 = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData2); 286 | 287 | const string responseData3 = "pew pew"; 288 | var messageResponse3 = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData3); 289 | 290 | var options = new List 291 | { 292 | new HttpMessageOptions 293 | { 294 | RequestUri = RequestUri, 295 | HttpResponseMessage = messageResponse1 296 | }, 297 | new HttpMessageOptions 298 | { 299 | RequestUri = new Uri("http://www.something.com/another/site"), 300 | HttpResponseMessage = messageResponse2 301 | }, 302 | new HttpMessageOptions 303 | { 304 | RequestUri = new Uri("http://www.whatever.com/"), 305 | HttpResponseMessage = messageResponse3 306 | }, 307 | }; 308 | 309 | var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); 310 | 311 | // Act & Assert. 312 | await DoGetAsync(RequestUri, 313 | ExpectedContent, 314 | fakeHttpMessageHandler); 315 | options[0].NumberOfTimesCalled.ShouldBe(1); 316 | options[1].NumberOfTimesCalled.ShouldBe(0); 317 | options[2].NumberOfTimesCalled.ShouldBe(0); 318 | } 319 | 320 | [Fact] 321 | public async Task GivenAnUnauthorisedStatusCodeResponse_GetAsync_ReturnsAFakeResponseWithAnUnauthorisedStatusCode() 322 | { 323 | // Arrange. 324 | var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage("pew pew", HttpStatusCode.Unauthorized); 325 | var options = new HttpMessageOptions 326 | { 327 | RequestUri = RequestUri, 328 | HttpResponseMessage = messageResponse 329 | }; 330 | var messageHandler = new FakeHttpMessageHandler(options); 331 | 332 | HttpResponseMessage message; 333 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 334 | { 335 | // Act. 336 | message = await httpClient.GetAsync(RequestUri); 337 | } 338 | 339 | // Assert. 340 | message.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); 341 | options.NumberOfTimesCalled.ShouldBe(1); 342 | } 343 | 344 | [Fact] 345 | public async Task GivenAValidHttpRequest_GetSomeDataAsync_ReturnsAFoo() 346 | { 347 | // Arrange. 348 | const string errorMessage = "Oh man - something bad happened."; 349 | var expectedException = new HttpRequestException(errorMessage); 350 | var messageHandler = new FakeHttpMessageHandler(expectedException); 351 | 352 | Exception exception; 353 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 354 | { 355 | // Act. 356 | // NOTE: network traffic will not leave your computer because you've faked the response, above. 357 | exception = await Should.ThrowAsync(async () => await httpClient.GetAsync(RequestUri)); 358 | } 359 | 360 | // Assert. 361 | exception.Message.ShouldBe(errorMessage); 362 | } 363 | 364 | [Fact] 365 | public async Task GivenAFewCallsToAnHttpRequest_GetSomeDataAsync_ReturnsAFakeResponse() 366 | { 367 | // Arrange. 368 | var httpResponseMessage = FakeHttpMessageHandler.GetStringHttpResponseMessage(ExpectedContent); 369 | var options = new HttpMessageOptions 370 | { 371 | HttpResponseMessage = httpResponseMessage 372 | }; 373 | var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); 374 | 375 | // Act & Assert 376 | await DoGetAsync(RequestUri, 377 | ExpectedContent, 378 | fakeHttpMessageHandler); 379 | await DoGetAsync(RequestUri, 380 | ExpectedContent, 381 | fakeHttpMessageHandler); 382 | await DoGetAsync(RequestUri, 383 | ExpectedContent, 384 | fakeHttpMessageHandler); 385 | options.NumberOfTimesCalled.ShouldBe(3); 386 | } 387 | 388 | [Theory] 389 | [MemberData(nameof(DifferentHttpMessageOptions))] 390 | public async Task GivenSomeDifferentHttpMessageOptions_GetAsync_ShouldThrowAnException(HttpMessageOptions options) 391 | { 392 | // Arrange. 393 | var fakeHttpMessageHandler = new FakeHttpMessageHandler(options); 394 | var headers = new Dictionary> 395 | { 396 | { 397 | "hi", new[] 398 | { 399 | "there" 400 | } 401 | } 402 | }; 403 | 404 | // Act. 405 | var exception = await Should.ThrowAsync(() => DoGetAsync(RequestUri, 406 | ExpectedContent, 407 | fakeHttpMessageHandler, 408 | headers)); 409 | 410 | // Assert. 411 | exception.Message.ShouldStartWith("No HttpResponseMessage found for the Request => "); 412 | options.NumberOfTimesCalled.ShouldBe(0); 413 | } 414 | 415 | private static async Task DoGetAsync(Uri requestUri, 416 | string expectedResponseContent, 417 | FakeHttpMessageHandler fakeHttpMessageHandler, 418 | IDictionary> optionalHeaders =null) 419 | { 420 | requestUri.ShouldNotBeNull(); 421 | expectedResponseContent.ShouldNotBeNullOrWhiteSpace(); 422 | fakeHttpMessageHandler.ShouldNotBeNull(); 423 | 424 | HttpResponseMessage message; 425 | string content; 426 | using (var httpClient = new System.Net.Http.HttpClient(fakeHttpMessageHandler)) 427 | { 428 | // Do we have any Headers? 429 | if (optionalHeaders != null && 430 | optionalHeaders.Any()) 431 | { 432 | foreach (var keyValue in optionalHeaders) 433 | { 434 | httpClient.DefaultRequestHeaders.Add(keyValue.Key, keyValue.Value); 435 | } 436 | } 437 | 438 | // Act. 439 | message = await httpClient.GetAsync(requestUri); 440 | content = await message.Content.ReadAsStringAsync(); 441 | } 442 | 443 | // Assert. 444 | message.StatusCode.ShouldBe(HttpStatusCode.OK); 445 | content.ShouldBe(expectedResponseContent); 446 | } 447 | } 448 | } -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/GetStringAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Shouldly; 5 | using WorldDomination.Net.Http; 6 | using Xunit; 7 | using System; 8 | 9 | // ReSharper disable ConsiderUsingConfigureAwait 10 | 11 | namespace WorldDomination.HttpClient.Helpers.Tests 12 | { 13 | public class GetStringAsyncTests 14 | { 15 | [Theory] 16 | [InlineData("http://www.something.com/some/website")] // Simple uri. 17 | [InlineData("http://www.something.com/some/website?a=1&b=2")] // Query string params. 18 | [InlineData("http://www.something.com/some/website?json={\"name\":\"hi\"}")] // Querystring content that needs to be encoded. 19 | public async Task GivenARequest_GetStringAsync_ReturnsAFakeResponse(string requestUri) 20 | { 21 | // Arrange. 22 | const string responseContent = "hi"; 23 | var options = new HttpMessageOptions 24 | { 25 | HttpMethod = HttpMethod.Get, 26 | RequestUri = new Uri(requestUri), 27 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) 28 | { 29 | Content = new StringContent(responseContent) 30 | } 31 | }; 32 | 33 | var messageHandler = new FakeHttpMessageHandler(options); 34 | 35 | string content; 36 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 37 | { 38 | // Act. 39 | content = await httpClient.GetStringAsync(requestUri); 40 | } 41 | 42 | // Assert. 43 | content.ShouldBe(responseContent); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/HttpClient.Helpers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/PostAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Shouldly; 8 | using WorldDomination.Net.Http; 9 | using Xunit; 10 | 11 | // ReSharper disable ConsiderUsingConfigureAwait 12 | 13 | namespace WorldDomination.HttpClient.Helpers.Tests 14 | { 15 | public class PostAsyncTests 16 | { 17 | public static IEnumerable ValidPostHttpContent 18 | { 19 | get 20 | { 21 | // Source content, Expected Content. 22 | // NOTE: we have to duplicate the source/expected below so we 23 | // test that they are **separate memory references** and not the same 24 | // memory reference. 25 | yield return new object[] 26 | { 27 | // Sample json. 28 | new StringContent("{\"id\":1}", Encoding.UTF8), 29 | new StringContent("{\"id\":1}", Encoding.UTF8) 30 | }; 31 | 32 | yield return new object[] 33 | { 34 | // Form key/values. 35 | new FormUrlEncodedContent(new[] 36 | { 37 | new KeyValuePair("a", "b"), 38 | new KeyValuePair("c", "1") 39 | }), 40 | new FormUrlEncodedContent(new[] 41 | { 42 | new KeyValuePair("a", "b"), 43 | new KeyValuePair("c", "1") 44 | }) 45 | }; 46 | } 47 | } 48 | 49 | public static IEnumerable InvalidPostHttpContent 50 | { 51 | get 52 | { 53 | yield return new object[] 54 | { 55 | // Sample json. 56 | new StringContent("{\"id\":1}", Encoding.UTF8), 57 | new StringContent("{\"id\":2}", Encoding.UTF8) 58 | }; 59 | 60 | yield return new object[] 61 | { 62 | // Sample json. 63 | new StringContent("{\"id\":1}", Encoding.UTF8), 64 | new StringContent("{\"ID\":1}", Encoding.UTF8) // Case has changed. 65 | }; 66 | 67 | yield return new object[] 68 | { 69 | // Form key/values. 70 | new FormUrlEncodedContent(new[] 71 | { 72 | new KeyValuePair("a", "b"), 73 | new KeyValuePair("c", "1") 74 | }), 75 | new FormUrlEncodedContent(new[] 76 | { 77 | new KeyValuePair("2", "1") 78 | }) 79 | }; 80 | } 81 | } 82 | 83 | [Theory] 84 | [MemberData(nameof(ValidPostHttpContent))] 85 | public async Task GivenAPostRequest_PostAsync_ReturnsAFakeResponse(HttpContent expectedHttpContent, 86 | HttpContent sentHttpContent) 87 | { 88 | // Arrange. 89 | Uri requestUri = new Uri("http://www.something.com/some/website"); 90 | const string responseContent = "hi"; 91 | var options = new HttpMessageOptions 92 | { 93 | HttpMethod = HttpMethod.Post, 94 | RequestUri = requestUri, 95 | HttpContent = expectedHttpContent, // This makes sure it's two separate memory references. 96 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) 97 | { 98 | Content = new StringContent(responseContent) 99 | } 100 | }; 101 | 102 | var messageHandler = new FakeHttpMessageHandler(options); 103 | 104 | HttpResponseMessage message; 105 | string content; 106 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 107 | { 108 | // Act. 109 | message = await httpClient.PostAsync(requestUri, sentHttpContent); 110 | content = await message.Content.ReadAsStringAsync(); 111 | } 112 | 113 | // Assert. 114 | message.StatusCode.ShouldBe(HttpStatusCode.OK); 115 | content.ShouldBe(responseContent); 116 | } 117 | 118 | [Theory] 119 | [MemberData(nameof(InvalidPostHttpContent))] 120 | public async Task GivenAPostRequestWithIncorrectlySetupOptions_PostAsync_ThrowsAnException(HttpContent expectedHttpContent, 121 | HttpContent sentHttpContent) 122 | { 123 | // Arrange. 124 | const string responseContent = "hi"; 125 | var options = new HttpMessageOptions 126 | { 127 | HttpMethod = HttpMethod.Post, 128 | RequestUri = new Uri("http://www.something.com/some/website"), 129 | HttpContent = expectedHttpContent, 130 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) 131 | { 132 | Content = new StringContent(responseContent) 133 | } 134 | }; 135 | 136 | var messageHandler = new FakeHttpMessageHandler(options); 137 | InvalidOperationException exception; 138 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 139 | { 140 | // Act. 141 | exception = 142 | await 143 | Should.ThrowAsync( 144 | async () => await httpClient.PostAsync("http://www.something.com/some/website", sentHttpContent)); 145 | } 146 | 147 | // Assert. 148 | exception.Message.ShouldStartWith("No HttpResponseMessage found"); 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/PutAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | using Shouldly; 10 | using WorldDomination.Net.Http; 11 | using Xunit; 12 | 13 | // ReSharper disable ConsiderUsingConfigureAwait 14 | 15 | namespace WorldDomination.HttpClient.Helpers.Tests 16 | { 17 | public class PutAsyncTests 18 | { 19 | public static IEnumerable ValidPutHttpContent 20 | { 21 | get 22 | { 23 | yield return new object[] 24 | { 25 | // Sample json. 26 | new StringContent("{\"id\":1}", Encoding.UTF8) 27 | }; 28 | 29 | yield return new object[] 30 | { 31 | new StringContent(JsonConvert.SerializeObject(DateTime.UtcNow.ToString("o")) , Encoding.UTF8) 32 | }; 33 | 34 | yield return new object[] 35 | { 36 | // Form key/values. 37 | new FormUrlEncodedContent(new[] 38 | { 39 | new KeyValuePair("a", "b"), 40 | new KeyValuePair("c", "1") 41 | }) 42 | }; 43 | } 44 | } 45 | 46 | public static IEnumerable VariousOptions 47 | { 48 | get 49 | { 50 | yield return new object[] 51 | { 52 | new [] 53 | { 54 | new HttpMessageOptions 55 | { 56 | HttpMethod = HttpMethod.Put, 57 | RequestUri = new Uri("http://www.something.com/some/website"), 58 | HttpContent = new StringContent(JsonConvert.SerializeObject(DateTime.UtcNow)), 59 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent) 60 | } 61 | } 62 | }; 63 | 64 | // Two options setup with two different Request Uri's. 65 | yield return new object[] 66 | { 67 | new [] 68 | { 69 | new HttpMessageOptions 70 | { 71 | HttpMethod = HttpMethod.Put, 72 | RequestUri = new Uri("http://www.something.com/some/website"), 73 | HttpContent = new StringContent(JsonConvert.SerializeObject(DateTime.UtcNow)), 74 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent) 75 | }, 76 | new HttpMessageOptions 77 | { 78 | HttpMethod = HttpMethod.Put, 79 | RequestUri = new Uri("http://www.1.2.3.4/a/b"), 80 | HttpContent = new StringContent(JsonConvert.SerializeObject(DateTime.UtcNow)), 81 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent) 82 | } 83 | } 84 | }; 85 | 86 | // Two options setup with two different Request Uri's. 87 | yield return new object[] 88 | { 89 | new [] 90 | { 91 | new HttpMessageOptions 92 | { 93 | HttpMethod = HttpMethod.Put, 94 | RequestUri = new Uri("http://www.something.com/some/website"), 95 | HttpContent = new StringContent(JsonConvert.SerializeObject(DateTime.UtcNow)), 96 | Headers = new Dictionary> 97 | { 98 | { 99 | "Bearer", new[] 100 | { 101 | "pewpew", 102 | "1234" 103 | } 104 | } 105 | }, 106 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent) 107 | }, 108 | 109 | new HttpMessageOptions 110 | { 111 | HttpMethod = HttpMethod.Put, 112 | RequestUri = new Uri("http://www.1.2.3.4/a/b"), 113 | HttpContent = new StringContent(JsonConvert.SerializeObject(DateTime.UtcNow)), 114 | Headers = new Dictionary> 115 | { 116 | { 117 | "Bearer", new[] 118 | { 119 | "pewpew", 120 | "1234" 121 | } 122 | } 123 | }, 124 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent) 125 | } 126 | } 127 | }; 128 | } 129 | } 130 | 131 | [Theory] 132 | [MemberData(nameof(ValidPutHttpContent))] 133 | public async Task GivenAPutRequest_PutAsync_ReturnsAFakeResponse(HttpContent content) 134 | { 135 | // Arrange. 136 | var requestUri = new Uri("http://www.something.com/some/website"); 137 | var options = new HttpMessageOptions 138 | { 139 | HttpMethod = HttpMethod.Put, 140 | RequestUri = requestUri, 141 | HttpContent = content, 142 | HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.NoContent) 143 | }; 144 | 145 | var messageHandler = new FakeHttpMessageHandler(options); 146 | 147 | HttpResponseMessage message; 148 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 149 | { 150 | // Act. 151 | message = await httpClient.PutAsync(requestUri, content); 152 | } 153 | 154 | // Assert. 155 | message.StatusCode.ShouldBe(HttpStatusCode.NoContent); 156 | } 157 | 158 | /// 159 | /// The actual httpClient call fails to match anything setup with the 'options'. 160 | /// 161 | [Theory] 162 | [MemberData(nameof(VariousOptions))] 163 | public async Task GivenADifferentPutRequestAndExpectedOutcome_PutAsync_ThrowsAnException(IEnumerable options) 164 | { 165 | // Arrange. 166 | var content = new StringContent("hi"); 167 | var messageHandler = new FakeHttpMessageHandler(options); 168 | 169 | InvalidOperationException exception; 170 | using (var httpClient = new System.Net.Http.HttpClient(messageHandler)) 171 | { 172 | // Act. 173 | exception = await Should.ThrowAsync( async () => await httpClient.PutAsync("http://a.b.c.d./abcde", content)); 174 | } 175 | 176 | // Act. 177 | exception.ShouldNotBeNull(); 178 | exception.Message.ShouldStartWith("No HttpResponseMessage found for the Request => What was called:"); 179 | exception.Message.ShouldContain($"{options.Count()}) "); // e.g. 1) or 2) .. etc. 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/Wiki Examples/Baa.cs: -------------------------------------------------------------------------------- 1 | namespace WorldDomination.HttpClient.Helpers.Tests.Wiki_Examples 2 | { 3 | public class Baa 4 | { 5 | public string FavGame { get; set; } 6 | public string FavMovie { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/Wiki Examples/Foo.cs: -------------------------------------------------------------------------------- 1 | namespace WorldDomination.HttpClient.Helpers.Tests.Wiki_Examples 2 | { 3 | public class Foo 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public Baa Baa { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/Wiki Examples/MultipleEndPointTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Shouldly; 3 | using WorldDomination.Net.Http; 4 | using Xunit; 5 | 6 | namespace WorldDomination.HttpClient.Helpers.Tests.Wiki_Examples 7 | { 8 | public class MultipleEndpointTests 9 | { 10 | [Fact] 11 | public async Task GivenSomeValidHttpRequests_GetSomeDataAsync_ReturnsAFoo() 12 | { 13 | // Arrange. 14 | 15 | // 1. First fake response. 16 | const string responseData1 = "{ \"Id\":69, \"Name\":\"Jane\" }"; 17 | var messageResponse1 = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData1); 18 | 19 | // 2. Second fake response. 20 | const string responseData2 = "{ \"FavGame\":\"Star Wars\", \"FavMovie\":\"Star Wars - all of em\" }"; 21 | var messageResponse2 = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData2); 22 | 23 | // Prepare our 'options' with all of the above fake stuff. 24 | var options = new[] 25 | { 26 | new HttpMessageOptions 27 | { 28 | RequestUri = MyService.GetFooEndPoint, 29 | HttpResponseMessage = messageResponse1 30 | }, 31 | new HttpMessageOptions 32 | { 33 | RequestUri = MyService.GetBaaEndPoint, 34 | HttpResponseMessage = messageResponse2 35 | } 36 | }; 37 | 38 | // 3. Use the fake responses if those urls are attempted. 39 | var messageHandler = new FakeHttpMessageHandler(options); 40 | 41 | var myService = new MyService(messageHandler); 42 | 43 | // Act. 44 | // NOTE: network traffic will not leave your computer because you've faked the response, above. 45 | var result = await myService.GetAllDataAsync(); 46 | 47 | // Assert. 48 | result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync. 49 | result.Baa.FavMovie.ShouldBe("Star Wars - all of em"); // Returned from GetSomeBaaDataAsync. 50 | options[0].NumberOfTimesCalled.ShouldBe(1); 51 | options[1].NumberOfTimesCalled.ShouldBe(1); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/Wiki Examples/MyService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Shouldly; 7 | using WorldDomination.Net.Http; 8 | 9 | namespace WorldDomination.HttpClient.Helpers.Tests.Wiki_Examples 10 | { 11 | public class MyService 12 | { 13 | public const string GetFooEndPoint = "http://www.something.com/some/website"; 14 | public const string GetBaaEndPoint = "http://www.something.com/another/site"; 15 | private readonly FakeHttpMessageHandler _messageHandler; 16 | 17 | public MyService(FakeHttpMessageHandler messageHandler = null) 18 | { 19 | _messageHandler = messageHandler; 20 | } 21 | 22 | public async Task GetAllDataAsync() 23 | { 24 | var foo = await GetSomeFooDataAsync(); 25 | foo.Baa = await GetSomeBaaDataAsync(); 26 | 27 | return foo; 28 | } 29 | 30 | public async Task GetSomeFooDataAsync() 31 | { 32 | return await GetSomeDataAsync(GetFooEndPoint); 33 | } 34 | 35 | public async Task GetSomeBaaDataAsync() 36 | { 37 | // NOTE: notice how this request endpoint is different to the one, above? 38 | return await GetSomeDataAsync(GetBaaEndPoint); 39 | } 40 | 41 | private async Task GetSomeDataAsync(string endPoint) 42 | { 43 | endPoint.ShouldNotBeNullOrWhiteSpace(); 44 | 45 | HttpResponseMessage message; 46 | string content; 47 | using (var httpClient = _messageHandler == null 48 | ? new System.Net.Http.HttpClient() 49 | : new System.Net.Http.HttpClient(_messageHandler)) 50 | { 51 | message = await httpClient.GetAsync(endPoint); 52 | content = await message.Content.ReadAsStringAsync(); 53 | } 54 | 55 | if (message.StatusCode != HttpStatusCode.OK) 56 | { 57 | // TODO: handle this ru-roh-error. 58 | throw new InvalidOperationException(content); 59 | } 60 | 61 | // Assumption: content is in a json format. 62 | return JsonConvert.DeserializeObject(content); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /tests/HttpClient.Helpers.Tests/Wiki Examples/SingleEndpointTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Shouldly; 5 | using WorldDomination.Net.Http; 6 | using Xunit; 7 | 8 | namespace WorldDomination.HttpClient.Helpers.Tests.Wiki_Examples 9 | { 10 | public class SingleEndpointTests 11 | { 12 | [Theory] 13 | [InlineData(MyService.GetFooEndPoint)] // Specific url they are hitting. 14 | [InlineData("*")] // Don't care what url they are hitting. 15 | public async Task GivenSomeValidHttpRequest_GetSomeFooDataAsync_ReturnsAFoo(string endPoint) 16 | { 17 | // Arrange. 18 | const string responseData = "{ \"Id\":69, \"Name\":\"Jane\" }"; 19 | var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData); 20 | 21 | var options = new HttpMessageOptions 22 | { 23 | RequestUri = endPoint, 24 | HttpResponseMessage = messageResponse 25 | }; 26 | 27 | var messageHandler = new FakeHttpMessageHandler(options); 28 | 29 | var myService = new MyService(messageHandler); 30 | 31 | // Act. 32 | // NOTE: network traffic will not leave your computer because you've faked the response, above. 33 | var result = await myService.GetSomeFooDataAsync(); 34 | 35 | // Assert. 36 | result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync. 37 | result.Baa.ShouldBeNull(); 38 | options.NumberOfTimesCalled.ShouldBe(1); 39 | } 40 | 41 | [Fact] 42 | public async Task GivenADifferentFakeUrlEndpoint_GetSomeFooDataAsync_ThrowsAnException() 43 | { 44 | // Arrange. 45 | const string responseData = "{ \"Id\":69, \"Name\":\"Jane\" }"; 46 | var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData); 47 | 48 | var options = new HttpMessageOptions 49 | { 50 | RequestUri = "http://this.is.not.the.correct.endpoint", 51 | HttpResponseMessage = messageResponse 52 | }; 53 | 54 | var messageHandler = new FakeHttpMessageHandler(options); 55 | 56 | var myService = new MyService(messageHandler); 57 | 58 | // Act. 59 | // NOTE: network traffic will not leave your computer because you've faked the response, above. 60 | var result = await Should.ThrowAsync(myService.GetSomeFooDataAsync()); 61 | 62 | // Assert. 63 | result.Message.ShouldStartWith("No HttpResponseMessage found"); 64 | } 65 | 66 | [Fact] 67 | public async Task GivenAServerError_GetSomeFooDataAsync_ThrowsAnException() 68 | { 69 | // Arrange. 70 | const string responseData = "Something Blew Up"; 71 | var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData, 72 | HttpStatusCode.InternalServerError); 73 | 74 | var options = new HttpMessageOptions 75 | { 76 | HttpResponseMessage = messageResponse 77 | }; 78 | var messageHandler = new FakeHttpMessageHandler(options); 79 | 80 | var myService = new MyService(messageHandler); 81 | 82 | // Act. 83 | // NOTE: network traffic will not leave your computer because you've faked the response, above. 84 | var result = await Should.ThrowAsync(myService.GetSomeFooDataAsync()); 85 | 86 | // Assert. 87 | result.Message.ShouldStartWith(responseData); 88 | } 89 | } 90 | } --------------------------------------------------------------------------------