{"id":15050657,"url":"https://github.com/alejandrohdezma/http4s-munit","last_synced_at":"2025-04-10T02:19:50.663Z","repository":{"id":37084516,"uuid":"315625096","full_name":"alejandrohdezma/http4s-munit","owner":"alejandrohdezma","description":"When http4s met MUnit","archived":false,"fork":false,"pushed_at":"2024-05-15T05:14:20.000Z","size":776,"stargazers_count":35,"open_issues_count":2,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-05-21T04:13:46.857Z","etag":null,"topics":["http4s","munit","scala","testing"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alejandrohdezma.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","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},"funding":{"ko_fi":"alejandrohdezma"}},"created_at":"2020-11-24T12:27:13.000Z","updated_at":"2024-08-10T12:00:16.764Z","dependencies_parsed_at":"2023-02-14T15:46:18.847Z","dependency_job_id":"fddb415e-0f3a-4289-aafe-90b84821226d","html_url":"https://github.com/alejandrohdezma/http4s-munit","commit_stats":null,"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fhttp4s-munit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fhttp4s-munit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fhttp4s-munit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alejandrohdezma%2Fhttp4s-munit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alejandrohdezma","download_url":"https://codeload.github.com/alejandrohdezma/http4s-munit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248142922,"owners_count":21054673,"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":["http4s","munit","scala","testing"],"created_at":"2024-09-24T21:28:46.966Z","updated_at":"2025-04-10T02:19:50.646Z","avatar_url":"https://github.com/alejandrohdezma.png","language":"Scala","funding_links":["https://ko-fi.com/alejandrohdezma"],"categories":[],"sub_categories":[],"readme":"# When http4s met MUnit\n\nIntegration library between [MUnit](https://scalameta.org/munit/) and [http4s](https://github.com/http4s/http4s/).\n\n---\n\n- [Installation](#installation)\n- [Contributors to this project](#contributors-to-this-project)\n- [Usage](#usage)\n  - [Testing `HttpRoutes`](#testing-httproutes)\n  - [Testing `AuthedRoutes`](#testing-authedroutes)\n  - [Using a mocked http4s `Client`](#using-a-mocked-http4s-client)\n  - [Testing a remote HTTP server](#testing-a-remote-http-server)\n  - [Testing an HTTP server running inside a container](#testing-an-http-server-running-inside-a-container)\n- [Other features](#other-features)\n  - [Running an effect before running your test](#running-an-effect-before-running-your-test)\n  - [Tagging your tests](#tagging-your-tests)\n  - [Stress-testing](#stress-testing)\n  - [Nested requests](#nested-requests)\n  - [Test names](#test-names)\n  - [Body in failed assertions](#body-in-failed-assertions)\n  - [Response clues](#response-clues)\n\n## Installation\n\nAdd the following line to your `build.sbt` file:\n\n```sbt\nlibraryDependencies += \"com.alejandrohdezma\" %% \"http4s-munit\" % \"1.1.0\" % Test\n```\n\n## Contributors to this project\n\n| \u003ca href=\"https://github.com/alejandrohdezma\"\u003e\u003cimg alt=\"alejandrohdezma\" src=\"https://avatars.githubusercontent.com/u/9027541?v=4\u0026s=120\" width=\"120px\" /\u003e\u003c/a\u003e | \u003ca href=\"https://github.com/gutiory\"\u003e\u003cimg alt=\"gutiory\" src=\"https://avatars.githubusercontent.com/u/3316502?v=4\u0026s=120\" width=\"120px\" /\u003e\u003c/a\u003e | \u003ca href=\"https://github.com/JackTreble\"\u003e\u003cimg alt=\"JackTreble\" src=\"https://avatars.githubusercontent.com/u/4872989?v=4\u0026s=120\" width=\"120px\" /\u003e\u003c/a\u003e |\n| :--: | :--: | :--: |\n| \u003ca href=\"https://github.com/alejandrohdezma\"\u003e\u003csub\u003e\u003cb\u003ealejandrohdezma\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e | \u003ca href=\"https://github.com/gutiory\"\u003e\u003csub\u003e\u003cb\u003egutiory\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e | \u003ca href=\"https://github.com/JackTreble\"\u003e\u003csub\u003e\u003cb\u003eJackTreble\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e |\n\n## Usage\n\nThis library provides a new type of suite (`Http4sSuite`) that you can use for\nseveral things:\n\n### Testing `HttpRoutes`\n\nWe can use the `Http4Suite` to write tests for an `HttpRoutes` using `Request[IO]` values easily:\n\n```scala\nimport cats.effect.IO\n\nimport org.http4s._\n\nclass MyHttpRoutesSuite extends munit.Http4sSuite {\n\n  override def http4sMUnitClientFixture = HttpRoutes.of[IO] {\n    case GET -\u003e Root / \"hello\"        =\u003e Ok(\"Hi\")\n    case GET -\u003e Root / \"hello\" / name =\u003e Ok(s\"Hi $name\")\n  }.orFail.asFixture\n\n  test(GET(uri\"hello\" / \"Jose\")).alias(\"Say hello to Jose\") { response =\u003e\n    assertIO(response.as[String], \"Hi Jose\")\n  }\n\n  // You can also override routes per-test\n  test(GET(uri\"hello\" / \"Jose\"))\n    .withHttpApp(HttpRoutes.of[IO] { case GET -\u003e Root / \"hello\" / _=\u003e Ok(\"Hi\") }.orFail)\n    .alias(\"Overriden routes\") { response =\u003e\n      assertIO(response.as[String], \"Hi\")\n    }\n\n}\n```\n\nThe `test` method receives a `Request[IO]` object and when the test runs, it runs that request against the provided routes and let you assert the response.\n\n`http4s-munit` will automatically name your tests using the information of the provided `Request`. For example, for the test shown in the previous code snippet, the following will be shown when running the test:\n\n```\nmunit.MyHttpRoutesSuite:0s\n  + GET -\u003e hello/Jose (Say hello to Jose) 0.014s\n```\n\n### Testing `AuthedRoutes`\n\nIf we want to test authenticated routes (`AuthedRoutes` in http4s) it will be\ncompletely similar to the previous section, except that we need to ensure we\nprovide the context in the request. The library provides a couple methods to\nsimplify this: `context` and `getContext`.\n\nFor both of them you need to have an implicit `Key[A]` instance (being `A`\nyour context's type) in scope.\n\n```scala\nimport cats.effect.IO\n\nimport org.http4s._\nimport org.typelevel.vault.Key\n\nclass MyAuthedRoutesSuite extends munit.Http4sSuite {\n\n  implicit val key: Key[String] = Key.newKey[IO, String].unsafeRunSync()\n\n  override def http4sMUnitClientFixture = AuthedRequest.fromContext[String].andThen {\n    AuthedRoutes.of[String, IO] {\n      case GET -\u003e Root / \"hello\" as user        =\u003e Ok(s\"$user: Hi\")\n      case GET -\u003e Root / \"hello\" / name as user =\u003e Ok(s\"$user: Hi $name\")\n    }\n  }.orFail.asFixture\n\n  test(GET(uri\"hello\" / \"Jose\").context(\"alex\")).alias(\"Say hello to Jose\") { response =\u003e\n    assertIO(response.as[String], \"alex: Hi Jose\")\n  }\n\n  // You can also override routes per-test\n  test(GET(uri\"hello\" / \"Jose\").context(\"alex\"))\n    .withHttpApp {\n      AuthedRequest.fromContext[String]\n        .andThen(AuthedRoutes.of[String, IO] { case GET -\u003e Root / \"hello\" / _ as _ =\u003e Ok(\"Hey\") })\n        .orFail\n    }\n    .alias(\"Overriden routes\") { response =\u003e\n      assertIO(response.as[String], \"Hey\")\n    }\n\n}\n```\n\n### Using a mocked http4s `Client`\n\nIf you just want to add tests for a class or algebra that uses a `Client` instance you can make your suite extend `Http4sMUnitSyntax` (it also requires extending `CatsEffectSuite`).\n\nIt includes a handful of utilities among which are two extension methods to the `Client` companion object: `from` and `partialFixture`.\n\n`Client.from` lets you create a mocked client from a partial function representing routes:\n\n```scala\nimport org.http4s.client.Client\n\nclass ClientSuiteSuite extends munit.CatsEffectSuite with munit.Http4sMUnitSyntax {\n\n  val client = Client.from {\n    case GET -\u003e Root / \"ping\" =\u003e Ok(\"pong\")\n  }\n\n}\n```\n\nOn the other hand, the class also provides another extension method: `Client.partialFixture`. This method is inteded to be used to easily create a fixture for testing a class that uses an http4s' `Client`.\n\nGiven an algebra like:\n\n```scala\nimport cats.effect._\nimport org.http4s.client.Client\n\ntrait PingService[F[_]] {\n\n  def ping(): F[String]\n\n}\n\nobject PingService {\n\n  def create[F[_]: Async](client: Client[F]) =\n    new PingService[F] {\n\n      def ping(): F[String] = client.expect[String](\"ping\")\n\n    }\n  \n\n}\n```\n\nYou can test it using `Http4sMUnitSyntax` like:\n\n```scala\nimport cats.effect._\nimport org.http4s.client.Client\n\nclass PingServiceSuite extends munit.CatsEffectSuite with munit.Http4sMUnitSyntax {\n\n  val fixture = Client.partialFixture(client =\u003e Resource.pure(PingService.create(client)))\n\n  fixture {\n    case GET -\u003e Root / \"ping\" =\u003e Ok(\"pong\")\n  }.test(\"PingService.ping works\") { service =\u003e\n    val result = service.ping()\n\n    assertIO(result, \"pong\")\n  }\n\n}\n```\n\n### Testing a remote HTTP server\n\nIn the case you don't want to use static http4s routes, but a running HTTP server,\nyou just need to provide a real http4s' `Client` implementation under `http4sMUnitClient`.\nEvery test request you write will be made using this client.\n\n```scala\nimport cats.effect.IO\nimport cats.effect.SyncIO\n\nimport io.circe.Json\nimport org.http4s.circe._\nimport org.http4s.client.Client\nimport org.http4s.ember.client.EmberClientBuilder\n\nclass GitHubSuite extends munit.Http4sSuite {\n\n  override def http4sMUnitClientFixture: SyncIO[FunFixture[Client[IO]]] =\n    ResourceFunFixture(EmberClientBuilder.default[IO].build.map(_.withBaseUri(uri\"https://api.github.com\")))\n\n  test(GET(uri\"users/gutiory\")) { response =\u003e\n    assertEquals(response.status.code, 200)\n\n    val result = response.as[Json].map(_.hcursor.get[String](\"login\"))\n\n    assertIO(result, Right(\"gutiory\"))\n  }\n\n}\n```\n\n\u003e If you are making requests to the same server, you can override `http4sMUnitClientFixture` like:\n\u003e\n\u003e ```scala\n\u003e override def http4sMUnitClientFixture: SyncIO[FunFixture[Client[IO]]] =\n\u003e   ResourceFunFixture(EmberClientBuilder.default[IO].build.map(_.withBaseUri(localhost.withPort(8080))))\n\u003e ```\n\n### Testing an HTTP server running inside a container\n\nTesting a Docker container with TestContainers and `http4s-munit` is easy. You\njust need to use `TestCotnainersFixtures` and use `Http4sSuite` to connect to\nit:\n\n```scala\nimport cats.effect.IO\nimport cats.effect.SyncIO\n\nimport com.dimafeng.testcontainers.GenericContainer\nimport com.dimafeng.testcontainers.munit.fixtures.TestContainersFixtures\nimport io.circe.Json\nimport org.http4s.client.Client\nimport org.http4s.circe._\nimport org.http4s.ember.client.EmberClientBuilder\n\nclass TestContainersSuite extends munit.Http4sSuite with TestContainersFixtures {\n\n  // There is also available `ForEachContainerFixture`\n  val container = ForAllContainerFixture {\n    GenericContainer(dockerImage = \"mendhak/http-https-echo\", exposedPorts = List(80))\n  }\n\n  override def munitFixtures = List(container)\n\n  override def http4sMUnitClientFixture: SyncIO[FunFixture[Client[IO]]] = ResourceFunFixture {\n    EmberClientBuilder.default[IO].build.map(_.withBaseUri(localhost.withPort(container().mappedPort(80))))\n  }\n\n  test(GET(uri\"ping\")) { response =\u003e\n    assertEquals(response.status.code, 200)\n    assertIOBoolean(response.as[Json].map(_.isObject))\n  }\n\n}\n```\n\nOr if you don't want to use container fixtures and you don't mind starting a container for each test:\n\n```scala\nimport cats.effect.IO\nimport cats.effect.Resource\nimport cats.syntax.all._\n\nimport com.dimafeng.testcontainers.GenericContainer\nimport org.http4s.ember.client.EmberClientBuilder\n\nclass TestContainersSuite extends munit.Http4sSuite {\n\n  lazy val container = GenericContainer(dockerImage = \"nginxdemos/hello\", exposedPorts = List(80))\n\n  override def http4sMUnitClientFixture = ResourceFunFixture {\n    Resource.fromAutoCloseable(IO(container.start()).as(container)) \u003e\u003e\n      EmberClientBuilder.default[IO].build.map(_.withBaseUri(localhost.withPort(container.mappedPort(80))))\n  }\n\n  test(GET(uri\"ping\")) { response =\u003e\n    assertEquals(response.status.code, 200, response.clues)\n  }\n\n}\n```\n\n## Other features\n\n### Running an effect before running your test\n\nSometimes (specially when you are testing against a real server) you need something to be\nrun before running your test. On these cases, you can just create a\n`ResourceFunFixture[Client[IO]]` (in which you can add other effects) and run it with `test`.\n\nEssentially this is the same as just running `test` since it is just an alias for\n`http4sMUnitClientFixture.test`.\n\n```scala\nimport cats.effect.IO\nimport cats.effect.Resource\nimport cats.syntax.all._\n\nimport io.circe.Json\nimport io.circe.syntax._\nimport org.http4s.ember.client.EmberClientBuilder\nimport org.http4s.circe._\n\nclass MyBookstoreSuite extends munit.Http4sSuite {\n\n  def httpClient = EmberClientBuilder.default[IO].build\n\n  override def http4sMUnitClientFixture = ResourceFunFixture(httpClient)\n\n  ResourceFunFixture {\n    httpClient.flatTap { client =\u003e\n      Resource.make {\n        val newBook = Json.obj(\"name\":= \"The Lord Of The Rings\")\n\n        client\n          .expect[Json](POST(newBook, uri\"http://localhost:8080/books\"))\n          .flatMap(_.hcursor.get[Int](\"id\").liftTo[IO])\n      } { id =\u003e\n        client.run(DELETE(uri\"http://localhost:8080/books\" / id)).use_\n      }.as(client)\n    }\n  }.test(GET(uri\"http://localhost:8080/books?q=Rings\")) { response =\u003e\n    assertEquals(response.status.code, 200, response.clues)\n\n    val result = response.as[Json].map(_.hcursor.get[String](\"name\"))\n\n    assertIO(result, Right(\"The Lord Of The Rings\"), response.clues)\n  }\n}\n```\n\n### Tagging your tests\n\nOnce the request has been passed to the `test` method, we can tag our tests before implementing them:\n\n\n```scala\n// Marks the test as failing (it will pass if the assertion fails)\ntest(GET(uri\"hello\")).fail { response =\u003e assertEquals(response.status.code, 200) }\n\n// Marks a test as \"flaky\". Check MUnit docs to know more about this feature:\n// https://scalameta.org/munit/docs/tests.html#tag-flaky-tests\ntest(GET(uri\"hello\")).flaky { response =\u003e assertEquals(response.status.code, 200) }\n\n// Skips this test when running the suite\ntest(GET(uri\"hello\")).ignore { response =\u003e assertEquals(response.status.code, 200) }\n\n// Runs only this test when running the suite\ntest(GET(uri\"hello\")).only { response =\u003e assertEquals(response.status.code, 200) }\n\n// We can also use our own tags, just like with MUnit `test`\nval IntegrationTest = new munit.Tag(\"integration-test\")\ntest(GET(uri\"hello\")).tag(IntegrationTest) { response =\u003e assertEquals(response.status.code, 200) }\n```\n\n### Stress-testing\n\n`http4s-munit` includes a small feature that allows you to \"stress-test\" a service. Once the request has been passed to the `test` method, we can call several methods to enable test repetition and parallelization:\n\n```scala\ntest(GET(uri\"hello\"))\n  .repeat(50)\n  .parallel(10) { response =\u003e \n    assertEquals(response.status.code, 200) \n  }\n```\n\nOn the other hand, if you do not want to have to call these methods for each test, you also have the possibility to enable repetition and parallelization using system properties or environment variables:\n\n- Using environment variables:\n\n  ```bash\n  export HTTP4S_MUNIT_REPETITIONS=50\n  export HTTP4S_MUNIT_MAX_PARALLEL=10\n\n  sbt test\n  ```\n\n- Using system properties:\n\n  ```bash\n  sbt -Dhttp4s.munit.repetitions=50 -Dhttp4s.munit.max.parallel=10 test\n  ```\n\nAlso, when multiple errors occured while running repeated tests, you can control wheter `http4s-munit` should output all failures or not using:\n\n```bash\n# Using environment variable\nexport HTTP4S_SHOW_ALL_STACK_TRACES=true\n\n# Using system property\nsbt -Dhttp4s.munit.showAllStackTraces=true test\n```\n\nFinally, if you want to disable repetitions for a specific test when using environment variables or system properties, you can use `doNotRepeat`:\n\n```scala\ntest(GET(uri\"hello\")).doNotRepeat { response =\u003e \n  assertEquals(response.status.code, 200) \n}\n```\n\n### Nested requests\n\nSometimes one test needs some pre-condition in order to be executed (e.g., in order to test the deletion of a user, you need to create it first). In such cases, once the request has been passed to the `test` method, we can call `andThen` to provide nested requests from the response of the previous one:\n\n```scala\ntest(GET(uri\"posts\" +? (\"number\" -\u003e 10)))\n    .alias(\"look for the 10th post\")\n    .andThen(\"delete it\")(_.as[String].map { id =\u003e\n      DELETE(uri\"posts\" / id)\n    }) { response =\u003e\n      assertEquals(response.status.code, 204)\n    }\n```\n\n### Test names\n\nThe generated test names can be customized by overriding `http4sMUnitTestNameCreator`. It allows altering the name of the generated tests.\n\nDefault implementation generates test names like:\n\n```scala\n// GET -\u003e users/42\ntest(GET(uri\"users\" / \"42\"))\n\n// GET -\u003e users (all users)\ntest(GET(uri\"users\")).alias(\"all users\")\n\n// GET -\u003e users - executed 10 times with 2 in parallel\ntest(GET(uri\"users\")).repeat(10).parallel(2)\n\n// GET -\u003e posts?number=10 (look for the 10th post and delete it)\ntest(GET(uri\"posts\" +? (\"number\" -\u003e 10)))\n    .alias(\"look for the 10th post\")\n    .andThen(\"delete it\")(_.as[String].map { id =\u003e DELETE(uri\"posts\" / id) })\n```\n\n### Body in failed assertions\n\n`http4s-munit` always includes the responses body in a failed assertion's message.\n\nFor example, when running the following suite...\n\n```scala\nimport cats.effect.IO\n\nimport org.http4s._\n\nclass MySuite extends munit.Http4sSuite {\n\n  override def http4sMUnitClientFixture = \n    HttpRoutes.of[IO](_ =\u003e Ok(\"\"\"{\"id\": 1, \"name\": \"Jose\"}\"\"\")).orFail.asFixture\n\n  test(GET(uri\"users\"))(response =\u003e assertEquals(response.status.code, 204))\n\n}\n```\n\n...it will fail with this message:\n\n```\nX MySuite.GET -\u003e users  0.042s munit.ComparisonFailException: MySuite.scala:12\n12:  test(GET(uri\"users\"))(response =\u003e assertEquals(response.status.code, 204))\nvalues are not the same\n=\u003e Obtained\n200\n=\u003e Diff (- obtained, + expected)\n-200\n+204\n\nResponse body was:\n\n{\n  \"id\": 1,\n  \"name\": \"Jose\"\n}\n```\n\nThe body will be prettified using `http4sMUnitBodyPrettifier`, which, by default, will try to parse it as JSON and apply a code highlight if `munitAnsiColors` is `true`. If you want a different output or disabling body-prettifying just override this method.\n\n### Response clues\n\nApart from the response body clues introduced in the previous section, `http4s-munit` also provides a simple way to transform a response into clues: the `response.clues` extension method.\n\nThe output of this extension method can be tweaked by using the `http4sMUnitResponseClueCreator`.\n\nFor example, this can be used on container suites to filter logs relevant to the current request (if your logs are JSON objects containing the request id):\n\n```scala\nimport cats.effect.IO\nimport cats.effect.Resource\nimport cats.syntax.all._\n\nimport com.dimafeng.testcontainers.GenericContainer\nimport io.circe.Json\nimport org.http4s._\nimport org.http4s.circe._\nimport org.http4s.ember.client.EmberClientBuilder\nimport org.typelevel.ci._\n\nclass TestContainersSuite extends munit.Http4sSuite {\n\n  override def http4sMUnitClientFixture = ResourceFunFixture {\n    Resource.fromAutoCloseable(IO(container.start()).as(container)) \u003e\u003e\n      EmberClientBuilder.default[IO].build.map(_.withBaseUri(localhost.withPort(container.mappedPort(80))))\n  }\n\n  override def http4sMUnitResponseClueCreator(response: Response[IO]) = {\n    val logs = response.headers\n      .get(ci\"x-request-id\")\n      .map(_.head.value)\n      .map(id =\u003e container.logs.split(\"\\n\").filter(_.contains(id)).mkString(\"\\n\"))\n      .getOrElse(container.logs)\n\n    clues(response, logs)\n  }\n\n  lazy val container = GenericContainer(dockerImage = \"mendhak/http-https-echo\", exposedPorts = List(80))\n\n  test(GET(uri\"ping\")) { response =\u003e\n    assertEquals(response.status.code, 200, response.clues)\n    assertIOBoolean(response.as[Json].map(_.isObject), response.clues)\n  }\n\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falejandrohdezma%2Fhttp4s-munit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falejandrohdezma%2Fhttp4s-munit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falejandrohdezma%2Fhttp4s-munit/lists"}