{"id":13611673,"url":"https://github.com/speekha/httpmocker","last_synced_at":"2025-04-13T05:33:18.538Z","repository":{"id":36908074,"uuid":"190229886","full_name":"speekha/httpmocker","owner":"speekha","description":"HttpMocker is a simple HTTP mocking library written in Kotlin to quickly and easily handle offline modes in your apps","archived":false,"fork":false,"pushed_at":"2022-01-28T10:19:42.000Z","size":1200,"stargazers_count":178,"open_issues_count":3,"forks_count":16,"subscribers_count":6,"default_branch":"develop","last_synced_at":"2024-11-07T18:48:02.319Z","etag":null,"topics":["hacktoberfest","http","kotlin","kotlin-library","ktor-client","mocking","okhttp4"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/speekha.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-04T15:32:39.000Z","updated_at":"2024-08-28T12:39:03.000Z","dependencies_parsed_at":"2022-09-01T03:10:41.098Z","dependency_job_id":null,"html_url":"https://github.com/speekha/httpmocker","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/speekha%2Fhttpmocker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/speekha%2Fhttpmocker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/speekha%2Fhttpmocker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/speekha%2Fhttpmocker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/speekha","download_url":"https://codeload.github.com/speekha/httpmocker/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248670513,"owners_count":21142896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["hacktoberfest","http","kotlin","kotlin-library","ktor-client","mocking","okhttp4"],"created_at":"2024-08-01T19:01:59.949Z","updated_at":"2025-04-13T05:33:13.523Z","avatar_url":"https://github.com/speekha.png","language":"Kotlin","funding_links":[],"categories":["测试","Kotlin"],"sub_categories":[],"readme":"# HttpMocker\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/fr.speekha.httpmocker/mocker-core/badge.svg?color=blue)](https://search.maven.org/search?q=fr.speekha.httpmocker)\n[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)\n[![Kotlin](https://img.shields.io/badge/kotlin-1.5.10-blue.svg?logo=kotlin)](http://kotlinlang.org)\n[![HacktoberFest](https://badgen.net/badge/hacktoberfest/friendly)](https://hacktoberfest.digitalocean.com/)\n\n[![CircleCI](https://circleci.com/gh/speekha/httpmocker/tree/develop.svg?style=shield)](https://circleci.com/gh/speekha/httpmocker/tree/develop)\n[![codebeat badge](https://codebeat.co/badges/a361e616-a1a0-4e85-b6a7-2bc82f31f7ac)](https://codebeat.co/projects/github-com-speekha-httpmocker-develop)\n[![CodeFactor](https://www.codefactor.io/repository/github/speekha/httpmocker/badge)](https://www.codefactor.io/repository/github/speekha/httpmocker)\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/754e2a65060a48c9bfc580a36063d206)](https://www.codacy.com/app/speekha/httpmocker)\n[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/754e2a65060a48c9bfc580a36063d206)](https://www.codacy.com/app/speekha/httpmocker)\n\n**HttpMocker** is a very lightweight Kotlin library that allows to mock HTTP calls relying on either\nOkHttp or the Ktor client libraries.\n\n* It can be used for unit or integration tests: by providing predefined responses and instead of actual \ncalls to servers, you're avoiding the risk of unpredictable results due to network failure or server errors. \n\n* It can also be used to implement an offline mode in your application for debugging or demo purposes:\nyou can prepare complete scenarios that will let users explore your app's features without the need for an actual\nconnection or account.\n\nThanks to the MockResponseInterceptor (for OkHttp) or the mockableHttpClient (for Ktor), web service \ncalls will not be dispatched to the network, but responses will be read from static configuration files\nor computed dynamically instead. The mocker also allows recording scenarios that can be reused later.\n\n## Current Version\n\n```gradle\nhttpmocker_version = '2.0.0-alpha'\n```\n\nCurrent version is stable for Android/JVM builds. It still has Alpha status because we would like to add support for iOS. \nAny help with implemention and deployment for iOS is welcome.\n\n## Gradle \n\n### Maven Central\n\nFor stable versions, check that you have the `mavenCentral` repository.\n\n```gradle\n// Add Jcenter to your repositories if needed\nrepositories {\n\tmavenCentral()\n}\n```\n\nFor Snapshot versions, check that you have Sonatype's snapshot repository.\n\n```gradle\n// Add oss.sonatype.org to your repositories if needed\nrepositories {\n    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }\n}\n```\n\n### Dependencies\n\n#### Adding HttpMocker\n\nThis library contains several parts: \n* a core module handling the mock logic\n* an engine module corresponding to the HTTP library you use (OkHttp or Ktor)\n* an additional adapter to parse the scenario files for static mocks\n\nYou can add the dependency to your `build.gradle` by adding on of the following lines:\n\n```gradle\n// Core module: you don't need to add it explicitly to your gradle (it will be included as a transitive dependency \n// of the other modules)\nimplementation \"fr.speekha.httpmocker:mocker-core:2.0.0-alpha\"\n\n// Handles mocks for OkHttp\nimplementation \"fr.speekha.httpmocker:mocker-okhttp:2.0.0-alpha\"\n\n// Handles mocks for KTor\nimplementation \"fr.speekha.httpmocker:mocker-ktor:2.0.0-alpha\"\n```\n\nCurrently, there are six possible options that are provided for parsing, based on some of the \nmost commonly used serialization libraries and on a custom implementation (no third party dependency): \n* Jackson\n* Gson\n* Moshi\n* Kotlinx serialization\n* Custom JSON implementation\n* Custom Sax-based implementation (uses XML scenarios instead of JSON)\n\nThis should allow you to choose one matching what you already use in your application (in order to prevent \nduplicate libraries in your classpath, like Jackson and GSON). If you would prefer to use XML instead \nof JSON, the SAX-based parser allows you to do it. If you choose one of these options, all you need to add is the \ncorresponding `implementation` line in your gradle file:\n\n```gradle\n// Parses JSON scenarios using Jackson\nimplementation \"fr.speekha.httpmocker:jackson-adapter:2.0.0-alpha\"\n\n// Parses JSON scenarios using Gson\nimplementation \"fr.speekha.httpmocker:gson-adapter:2.0.0-alpha\"\n\n// Parses JSON scenarios using Moshi\nimplementation \"fr.speekha.httpmocker:moshi-adapter:2.0.0-alpha\"\n\n// Parses JSON scenarios using Kotlinx Serialization\nimplementation \"fr.speekha.httpmocker:kotlinx-adapter:2.0.0-alpha\"\n\n// Parses JSON scenarios using a custom JSON parser\nimplementation \"fr.speekha.httpmocker:custom-adapter:2.0.0-alpha\"\n\n// Parses XML scenarios using a custom SAX parser\nimplementation \"fr.speekha.httpmocker:sax-adapter:2.0.0-alpha\"\n```\n\nIf none of those options suit your needs, you can also provide your own implementation of the `Mapper` class. You can \nalso bypass that dependency altogether if you don't plan on using static mocks or the recording mode.\n\n#### External dependencies\n\n* HttpMocker is a mocking library for third party HTTP clients, so it depends on OkHttp (as of v1.3.0, HttpMocker uses \nOkHttp 4 API, previous versions used OkHttp 3) or Ktor (v1.5.0), depending on which implementation you chose.\n* It also uses SLF4J API for logging.\n* JSON parsers depend on their respective external libraries: Jackson, Gson, Moshi or KotlinX serialization.\n\n### Proguard rules\n\nSince Jackson, Gson or Kotlinx serialization use some type of introspection or annotation \nprocessing to parse JSON streams, it is recommended to keep the mapping classes unobfuscated. \nYou can refer to those libraries recommendations and to the [proguard-rules.pro](demo/proguard-rules.pro) file included \nin the demo app for an example of required rules.\n\nThe custom and moshi parsers are immune to obfuscation because they do not use any introspection.\n\n## Quickstart\n\n### Setting up HttpMocker with OkHttp\n\nMocking HTTP calls relies on a simple Interceptor: MockResponseInterceptor. All you need to set it up\nis to add it to your OkHttp client. Here's an example with minimal configuration of dynamic mocks \n(this configuration will respond to every request with a `HTTP 200 Ok` code and a body containing \nonly `Fake response body`) \n* __Java-friendly builder syntax__:\n```kotlin\n    val interceptor = Builder()\n        .useDynamicMocks {\n            ResponseDescriptor(code = 200, body = \"Fake response body\")\n        }\n        .setMode(ENABLED)\n        .build()\n    val client = OkHttpClient.Builder()\n        .addInterceptor(interceptor)\n        .build()\n```\n\n* **Kotlin DSL syntax**:\n```kotlin\n    val interceptor = mockInterceptor {\n        useDynamicMocks {\n            ResponseDescriptor(code = 200, body = \"Fake response body\")\n        }\n        setMode(ENABLED)\n    }\n    val client = OkHttpClient.Builder()\n        .addInterceptor(interceptor)\n        .build()\n```\n\n### Setting up HttpMocker with Ktor\n\nMocking HTTP calls relies on a HTTP client that will include two engines: one for actual network calls and one for mocked\ncalls. The syntax to create this client is very similar to the one for the standard HTTP client, but uses a \n`mockableHttpClient` builder instead of the usual `httpClient`. You can keep all your usual configuration and just add \nthe mock configuration inside a `mock` section. Here is an example that shows how the mock configuration is added to a \nregular Ktor client (with a CIO engine and JSON parsing with Kotlinx Serialization):\n```kotlin\n    val client = mockableHttpClient(CIO) {\n        // This part defines the mock configuration to use\n        mock {\n            useDynamicMocks {\n                ResponseDescriptor(code = 200, body = \"Fake response body\")\n            }\n            setMode(ENABLED)\n        }\n\n        // Here is the rest of your standard HTTP Configuration\n        install(JsonFeature) {\n            serializer = KotlinxSerializer()\n        }\n        expectSuccess = false\n        followRedirects = false\n    }\n```\n\n### General use\n\nHttpMocker supports four modes:\n* Disabled\n* Enabled\n* Mixed\n* Record\n\nYou can set that mode when initializing your mocker object (if not, it is disabled by default), but you \ncan also change its value dynamically later on.\n\n```kotlin\n    // For OkHttp, you can change the mode directly in your interceptor: \n    interceptor.mode = ENABLED\n\n    // For Ktor, you will need to access the engine configuration:\n    (client.engine as MockEngine).mode = ENABLED\n```\n\nIf your mocker is `disabled`, it will not interfere with actual network calls. If it is `enabled`, \nit will need to find scenarios to mock the HTTP calls. Dynamic mocks imply that you have to \nprovide the response for each request programmatically, which allows you to define stateful \nresponses (identical calls could lead to different answers based on what the user did in between \nthese calls, or on any other external parameter you want to use: time, location...). The response \ncan be provided as a `ResponseDescriptor` by implementing the `RequestCallback` interface, or you \ncan simply provide a lambda function to do the computation. \n\n*NB: If you use dynamic mocks, the `bodyFile` attribute of your `ResponseDescriptor` is not needed (it \nwill be ignored). Its only use is for static scenarios that could save the response body in a separate \nfile (it keeps things clearer by not including the response file mixed with the scenario).*\n\nAnother option is to use static mocks. Static mocks are scenarios stored as static files. Here is \nan example for an Android app using static mocks, with a few more options:\n```kotlin\n    // For OkHttp:\n    val interceptor = mockInterceptor {\n            parseScenariosWith(mapper)\n            decodeScenarioPathWith(MirrorPathPolicy(\"json\"))\n            loadFileWith { \n                context.assets.open(it).asReader()\n            }\n            setMode(ENABLED)\n            recordScenariosIn(rootFolder)\n            addFakeNetworkDelay(50L)\n        }\n\n    // For Ktor:\n    val client = mockableHttpClient(CIO) {\n            mock {\n                parseScenariosWith(mapper)\n                decodeScenarioPathWith(MirrorPathPolicy(\"json\"))\n                loadFileWith { \n                    context.assets.open(it).asReader()\n                }\n                setMode(ENABLED)\n                recordScenariosIn(rootFolder)\n                addFakeNetworkDelay(50L)\n            }\n        }\n```\n\nIn this example, we decided to store the scenarios in the `assets` folder of the app (but you \ncould also have them as resources in your classpath and use the `Classloader` to access them or \neven store them in a certain folder and access that folder with any File API you're comfortable \nwith). You also need to provide the `FilingPolicy` you want to use: that policy defines which file to \ncheck to find a match for a request. A few policies are provided in the library, but you can also \ndefine your own. \n\nAdditionally, you need to provide a Mapper to parse the scenario files. As stated earlier, scenarios \ncan be stored as JSON or XML depending on your preference, or any other format, for that matter,\nas long as you provide your own `Mapper` class to serialize and deserialize the business objects. As \nfar as this lib is concerned though, a few mappers are available out of the box to handle JSON or \nXML formats. The main mappers are based on Jackson, Gson, Moshi and Kotlinx serialization. They \nare provided in specific modules so you can choose one based on the library you might already use \nin your application, thus limiting the risk for duplicate libraries serving the same purpose in \nyour app. Two additional implementations, based on a custom JSON or XML parsers that do not need \nany other dependencies are also available. \n\nStatic and dynamic scenarios can be used together. Several dynamic callbacks can be added to the \nmocker, but only one static configuration is allowed. Dynamic callbacks will be used first to \nfind a suitable mock, and if none is found, the static configuration will be tested next.\n\nIf you choose the `mixed` mode, requests that can not be answered by a predefined scenario will \nactually be executed. Hence the mixed mode: responses can come from a mock scenario (either static or \ndynamic) or from an actual HTTP call.\n\nFinally, the mocker also has a `recording` mode. This mode allows you to record scenarios \nwithout interfering with your request. If you choose this mode to produce your static scenarios, \nyou will have to provide a root folder where the scenarios should be stored. Also, you should \nrealize that all request and response attributes will be recorded. Generally, you will want to \nreview the resulting scenarios and clean them up a bit manually. For instance, each request will be \nrecorded with its HTTP method, its path, each header or parameter, its body. But all those details \nmight not be very important to you. Maybe all you care about is the URL and method, in which case, \nyou can delete all the superfluous criteria manually.\n\n## Building static scenarios\n\nAnswering a request with a static mock is done in two steps:\n* First, if the mocker is enabled (or in mixed mode), it will try to compute a file name were the appropriate \nscenario should be stored. Based on the filing policy you chose, those files can be organized in a lot of different \nways: all in a single file, or different files in the same folder, in separate folders matching the URL path,\nignoring or not the server hostname. This part allows you to decide how you want to organize your mock files for an \neasier navigation (if you have lots of scenarios to handle, the Single File policy will probably not be the best option).\n\n* Second, once the file is found, its content will be loaded, and a more exact match will have to be found. Scenario \nfiles contain a list of \"Matchers\", that is a list of request patterns and corresponding responses. Based on the \nrequest it is trying to answer, the mocker is going to scan through all the request declarations and stop as soon\nas it finds one that matches the situation.\n\nWhen writing a request pattern, the elements included are supposed to be found in the requests to match. The more \nelements, the more precise the match has to be. The less elements, the more permissive the match. A request can even \nbe omitted altogether (in this case, all requests match):\n* When specifying a method, matching request have to use the same HTTP method.\n* When specifying query parameters, matching requests must have at least all these parameters, but can have more.\n* When specifying headers, matching requests must have all the same headers, but can have more.\n\n*NB: A strict matching flag can be added in your scenarios that will force query parameters and headers to be exact \nmatches: if your request includes additional query parameters or headers that are not included in the scenario, it \nwill not match.*\n\nHere is an example of scenario in JSON form:\n \n```json\n[\n  {\n    \"request\": {\n      \"method\": \"post\",\n      \"headers\": {\n        \"myHeader\": \"myHeaderValue\"\n      },\n      \"params\": {\n        \"myParam\": \"myParamValue\"\n      },\n      \"body\": \".*1.*\"\n    },\n    \"response\": {\n      \"delay\": 50,\n      \"code\": 200,\n      \"media-type\": \"application/json\",\n      \"headers\": {\n        \"myHeader\": \"headerValue1\"\n      },\n      \"body-file\": \"body_content.txt\"\n    }\n  }, {\n       \"response\": {\n         \"delay\": 50,\n         \"code\": 200,\n         \"media-type\": \"application/json\",\n         \"headers\": {\n           \"myHeader\": \"headerValue2\"\n         },\n         \"body\": \"No body here\"\n       }\n  }\n]\n```\n\nHere is the same example in XML format:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cscenarios\u003e\n    \u003ccase\u003e\n        \u003crequest\u003e\n            \u003curl method=\"post\"\u003e\n                \u003cparam name=\"myParam\"\u003emyParamValue\u003c/param\u003e\n            \u003c/url\u003e\n            \u003cheader name=\"myHeader\"\u003emyHeaderValue\u003c/header\u003e\n            \u003cbody\u003e.*1.*\u003c/body\u003e\n        \u003c/request\u003e\n        \u003cresponse delay=\"50\" code=\"200\" media-type=\"application/json\"\u003e\n            \u003cheader name=\"myHeader\"\u003eheaderValue1\u003c/header\u003e\n            \u003cbody file=\"body_content.txt\" /\u003e\n        \u003c/response\u003e\n    \u003c/case\u003e\n    \u003ccase\u003e\n        \u003cresponse delay=\"50\" code=\"200\" media-type=\"application/json\"\u003e\n            \u003cheader name=\"myHeader\"\u003eheaderValue2\u003c/header\u003e\n            \u003cbody\u003eNo body here\u003c/body\u003e\n        \u003c/response\u003e\n    \u003c/case\u003e\n\u003c/scenarios\u003e\n```\n\nIn this example, a POST request on the corresponding URL, including a query param `myParam` with the value \n`myParamValue`, a header `myHeader` with the value `myHeaderValue` and a body containing the digit `1` (based on \nthe regex used as body) will match the first case: it will be answered with an HTTP 200 response of type \n`application/json`, with a header `myHeader` of value `headerValue1`. The body for this response will be found in \na nearby file named `body_content.txt`. In any other cases, the request will be answered with the second response:\na HTTP 200 response with `headerValue2` as header and a simple string `No body here` as body.\n\nFinally, in some cases, network connections might fail with an exception instead of an HTTP error code. This kind\nof scenario can also be mocked quite simply. All that is required is to replace the response section by an error \none, as in the example below:\n\n```json\n[\n  {\n    \"request\": {\n      \"method\": \"get\"\n    },\n    \"error\": {\n      \"type\": \"java.io.IOException\",\n      \"message\": \"Connection was reset by server\"\n    }\n  }\n]\n```\nThe corresponding XML version would go like this:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cscenarios\u003e\n    \u003ccase\u003e\n        \u003crequest\u003e\n            \u003curl method=\"get\" /\u003e\n        \u003c/request\u003e\n        \u003cerror type=\"java.io.IOException\"\u003eConnection was reset by server\u003c/error\u003e\n    \u003c/case\u003e\n\u003c/scenarios\u003e\n```\nIn that case, the mocker will try to instantiate an exception matching the type declared in your scenario.\n\n## Author\n\n[![Follow me](https://avatars.githubusercontent.com/u/17544216?s=60)](https://twitter.com/speekha)\n\n[![Follow me](https://img.shields.io/twitter/follow/speekha?style=social)](https://twitter.com/speekha)\n\n## Publications\n\n[Introductory blog post on Kt. Academy](https://blog.kotlin-academy.com/httpmock-my-first-oss-library-5bae8adbccf4)\n\n[Kotlin Weekly](https://mailchi.mp/kotlinweekly/kotlin-weekly-155) and \n[Android Weekly](https://androidweekly.net/issues/issue-371) mentioned it\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspeekha%2Fhttpmocker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspeekha%2Fhttpmocker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspeekha%2Fhttpmocker/lists"}