Skip to content

Commit

Permalink
Merge pull request #246 from JackTreble/main
Browse files Browse the repository at this point in the history
add Http4sTestHttpRoutesSuite & Http4sTestAuthedRoutesSuite
  • Loading branch information
alejandrohdezma authored Aug 24, 2022
2 parents 4aaff61 + b9d7a9c commit 6f57812
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 14 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ munit.MyHttpRoutesSuite:0s
+ GET -> hello/Jose (Say hello to Jose) 0.014s
```

### Testing `HttpRoutes` per-test

We can use the `Http4sTestHttpRoutesSuite` to write tests for an `HttpRoutes` using `Request[IO]` values easily:

```scala
import cats.effect.IO

import org.http4s._

class MyHttpRoutesSuite extends munit.Http4sTestHttpRoutesSuite {

test(
routes = {
HttpRoutes.of { case GET -> Root / "hello" / name =>
Ok(s"Hi $name")
}
}
)(GET(uri"hello" / "Jose")).alias("Say hello to Jose") { response =>
assertIO(response.as[String], "Hi Jose")
}

}
```

The `test` method receives `HttpRoutes[IO]` and `Request[IO]` object and when the test runs, it runs that request against the provided routes and let you assert the response.

`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:

```
munit.MyHttpRoutesSuite:0s
+ GET -> hello/Jose (Say hello to Jose) 0.014s
```

### Testing `AuthedRoutes`

If we want to test authenticated routes (`AuthedRoutes` in http4s) we can use the `Http4sAuthedRoutesSuite`. It is completely similar to the previous suite, except that we need to ensure a `Show` instance is available for the auth "context" type and that we need to provide `AuthedRequest` instead of `Request` in the `test` definition. We can do this using its own constructor or by using our extension function `context` or `->`:
Expand All @@ -83,6 +116,26 @@ class MyAuthedRoutesSuite extends munit.Http4sAuthedRoutesSuite[String] {
}
```

### Testing `AuthedRoutes` per-test

If we want to test authenticated routes (`AuthedRoutes` in http4s) we can use the `Http4sTestAuthedRoutesSuite`. It is completely similar to the previous suite, except that we need to ensure a `Show` instance is available for the auth "context" type and that we need to provide `AuthedRequest` instead of `Request` in the `test` definition. We can do this using its own constructor or by using our extension function `context` or `->`:

```scala
import cats.effect.IO

import org.http4s._

class MyAuthedRoutesSuite extends munit.Http4sTestAuthedRoutesSuite[String] {

test(routes = AuthedRoutes.of { case GET -> Root / "hello" / name as user =>
Ok(s"$user: Hi $name")
})(GET(uri"hello" / "Jose").context("alex")).alias("Say hello to Jose") { response =>
assertIO(response.as[String], "alex: Hi Jose")
}

}
```

### Testing a remote HTTP server

In the case you don't want to use static http4s routes, but a running HTTP server, you have available the `HttpSuite`. This suite behaves exactly the same as the previous ones except that you don't provide a `routes` object, but a `baseUri` with the URI of your HTTP server. Any `Request` added in tests will prepend
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ abstract class Http4sAuthedRoutesSuite[A: Show] extends Http4sSuite[AuthedReques

}

override def http4sMUnitFunFixture: SyncIO[FunFixture[ContextRequest[IO, A] => Resource[IO, Response[IO]]]] =
def http4sMUnitFunFixture: SyncIO[FunFixture[ContextRequest[IO, A] => Resource[IO, Response[IO]]]] =
SyncIO.pure(FunFixture(_ => routes.orNotFound.run(_).to[Resource[IO, *]], _ => ()))

/** Declares a test for the provided request. That request will be executed using the routes provided in `routes`.
Expand All @@ -108,6 +108,7 @@ abstract class Http4sAuthedRoutesSuite[A: Show] extends Http4sSuite[AuthedReques
* }
* }}}
*/
def test(request: AuthedRequest[IO, A]): Http4sMUnitTestCreator = Http4sMUnitTestCreator(request)
def test(request: AuthedRequest[IO, A]): Http4sMUnitTestCreator =
Http4sMUnitTestCreator(request, http4sMUnitFunFixture)

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ abstract class Http4sHttpRoutesSuite extends Http4sSuite[Request[IO]] {
http4sMUnitNameCreatorReplacements()
)

override def http4sMUnitFunFixture: SyncIO[FunFixture[Request[IO] => Resource[IO, Response[IO]]]] =
def http4sMUnitFunFixture: SyncIO[FunFixture[Request[IO] => Resource[IO, Response[IO]]]] =
SyncIO.pure(FunFixture(_ => req => routes.orNotFound.run(req).to[Resource[IO, *]], _ => ()))

/** Declares a test for the provided request. That request will be executed using the routes provided in `routes`.
Expand All @@ -99,6 +99,6 @@ abstract class Http4sHttpRoutesSuite extends Http4sSuite[Request[IO]] {
* }
* }}}
*/
def test(request: Request[IO]): Http4sMUnitTestCreator = Http4sMUnitTestCreator(request)
def test(request: Request[IO]): Http4sMUnitTestCreator = Http4sMUnitTestCreator(request, http4sMUnitFunFixture)

}
6 changes: 1 addition & 5 deletions modules/http4s-munit/src/main/scala/munit/Http4sSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,6 @@ abstract class Http4sSuite[Request] extends CatsEffectSuite with Http4sDsl[IO] w
else json
)

/** Base fixture used to obtain a response from a request. Can be re-implemented if you want to override the default
* behaviour of a suite.
*/
def http4sMUnitFunFixture: SyncIO[FunFixture[Request => Resource[IO, Response[IO]]]]

implicit final class CiStringHeaderOps(ci: CIString) {

/** Creates a `Header.Raw` value from a case-insensitive string. */
Expand All @@ -149,6 +144,7 @@ abstract class Http4sSuite[Request] extends CatsEffectSuite with Http4sDsl[IO] w

case class Http4sMUnitTestCreator(
request: Request,
http4sMUnitFunFixture: SyncIO[FunFixture[Request => Resource[IO, Response[IO]]]],
followingRequests: List[(String, Response[IO] => IO[Request])] = Nil,
testOptions: TestOptions = TestOptions(""),
config: Http4sMUnitConfig = Http4sMUnitConfig.default
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2020-2022 Alejandro Hernández <https://github.com/alejandrohdezma>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package munit

import cats.Show
import cats.effect.IO
import cats.effect.Resource
import cats.effect.SyncIO
import org.http4s.AuthedRequest
import org.http4s.AuthedRoutes
import org.http4s.ContextRequest
import org.http4s.Request
import org.http4s.Response

/** Base class for suites testing per-test `AuthedRoutes`. * Ensure that a `Show` instance for the request's context
* type is in scope. This instance will be used to include the context's information in the test's name.
*
* @example
* {{{
* import cats.effect.IO
*
* import org.http4s.AuthedRoutes
*
* class MyAuthedRoutesSuite extends munit.Http4sTestAuthedRoutesSuite[String] {
*
* test(routes = AuthedRoutes.of { case GET -> Root / "hello" as user =>
* Ok(user + " says Hi")
* }).as("Jose")) { response =>
* assertIO(response.as[String], "Jose says Hi")
* }
*
* }
* }}}
*/
abstract class Http4sTestAuthedRoutesSuite[A: Show] extends Http4sSuite[AuthedRequest[IO, A]] {

/** @inheritdoc */
override def http4sMUnitNameCreator(
request: AuthedRequest[IO, A],
followingRequests: List[String],
testOptions: TestOptions,
config: Http4sMUnitConfig
): String = Http4sMUnitDefaults.http4sMUnitNameCreator(
request,
followingRequests,
testOptions,
config,
http4sMUnitNameCreatorReplacements()
)

implicit class Request2AuthedRequest(request: Request[IO]) {

/** Converts an `IO[Request[IO]]` into an `IO[AuthedRequest[IO, A]]` by providing the `A` context. */
def context(context: A): AuthedRequest[IO, A] = AuthedRequest(context, request)

/** Converts an `IO[Request[IO]]` into an `IO[AuthedRequest[IO, A]]` by providing the `A` context. */
def ->(a: A): AuthedRequest[IO, A] = context(a)

}

def http4sMUnitFunFixture(
routes: AuthedRoutes[A, IO]
): SyncIO[FunFixture[ContextRequest[IO, A] => Resource[IO, Response[IO]]]] =
SyncIO.pure(FunFixture(_ => routes.orNotFound.run(_).to[Resource[IO, *]], _ => ()))

/** Declares a test for the provided routes and request.
*
* @example
* {{{
* test(routes = AuthedRoutes.of { case GET -> Root / "users" / number =>
* Ok(number)
* }(GET(uri"users" / 42)) { response =>
* // test body
* }
* }}}
* @example
* {{{
* test(routes = AuthedRoutes.of { case req @ POST -> Root / "users" =>
* Ok(req.as[String])
* }(POST(json, uri"users")).alias("Create a new user") { response =>
* // test body
* }
* }}}
* @example
* {{{
* test(routes = AuthedRoutes.of { case GET -> Root / "users" / number =>
* Ok(number)
* }(GET(uri"users" / 42)).flaky { response =>
* // test body
* }
* }}}
*/
def test(routes: AuthedRoutes[A, IO])(request: AuthedRequest[IO, A]): Http4sMUnitTestCreator =
Http4sMUnitTestCreator(request, http4sMUnitFunFixture(routes))

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2020-2022 Alejandro Hernández <https://github.com/alejandrohdezma>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package munit

import cats.effect.IO
import cats.effect.Resource
import cats.effect.SyncIO

import org.http4s.ContextRequest
import org.http4s.HttpRoutes
import org.http4s.Request
import org.http4s.Response

/** Base class for suites testing per-test `HttpRoutes`.
*
* @example
* {{{
* import cats.effect.IO
*
* import org.http4s.HttpRoutes
*
* class MyHttpRoutesSuite extends munit.Http4sTestHttpRoutesSuite {
*
* test(routes = HttpRoutes.of { case GET -> Root / "hello" =>
* Ok("Hello!")
* }(GET(uri"hello")) { response =>
* assertIO(response.as[String], "Hello!")
* }
*
* }
* }}}
* @author
* Jack Treble
*/
abstract class Http4sTestHttpRoutesSuite extends Http4sSuite[Request[IO]] {

/** @inheritdoc */
override def http4sMUnitNameCreator(
request: Request[IO],
followingRequests: List[String],
testOptions: TestOptions,
config: Http4sMUnitConfig
): String =
Http4sMUnitDefaults.http4sMUnitNameCreator(
ContextRequest((), request),
followingRequests,
testOptions,
config,
http4sMUnitNameCreatorReplacements()
)

def http4sMUnitFunFixture(routes: HttpRoutes[IO]): SyncIO[FunFixture[Request[IO] => Resource[IO, Response[IO]]]] =
SyncIO.pure(FunFixture(_ => req => routes.orNotFound.run(req).to[Resource[IO, *]], _ => ()))

/** Declares a test for the provided routes and request.
*
* @example
* {{{
* test(routes = HttpRoutes.of { case GET -> Root / "users" / number =>
* Ok(number)
* }(GET(uri"users" / 42)) { response =>
* // test body
* }
* }}}
* @example
* {{{
* test(routes = HttpRoutes.of { case req @ POST -> Root / "users" =>
* Ok(req.as[String])
* }(POST(json, uri"users")).alias("Create a new user") { response =>
* // test body
* }
* }}}
* @example
* {{{
* test(routes = HttpRoutes.of { case GET -> Root / "users" / number =>
* Ok(number)
* }(GET(uri"users" / 42)).flaky { response =>
* // test body
* }
* }}}
*/
def test(routes: HttpRoutes[IO])(request: Request[IO]): Http4sMUnitTestCreator = {
Http4sMUnitTestCreator(
request,
http4sMUnitFunFixture(routes)
)
}

}
4 changes: 2 additions & 2 deletions modules/http4s-munit/src/main/scala/munit/HttpSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ abstract class HttpSuite extends Http4sSuite[Request[IO]] with CatsEffectFunFixt

}

override def http4sMUnitFunFixture: SyncIO[FunFixture[Request[IO] => Resource[IO, Response[IO]]]] =
def http4sMUnitFunFixture: SyncIO[FunFixture[Request[IO] => Resource[IO, Response[IO]]]] =
ResourceFixture(http4sMUnitClient.map(client => req => client.run(req.withUri(baseUri().resolve(req.uri)))))

/** Declares a test for the provided request. That request will be executed using the provided client in `httpClient`
Expand All @@ -139,6 +139,6 @@ abstract class HttpSuite extends Http4sSuite[Request[IO]] with CatsEffectFunFixt
* }
* }}}
*/
def test(request: Request[IO]) = Http4sMUnitTestCreator(request)
def test(request: Request[IO]) = Http4sMUnitTestCreator(request, http4sMUnitFunFixture)

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ class HeaderInterpolatorSuite extends Http4sSuite[String] {
config: Http4sMUnitConfig
): String = fail("This should no be called")

override def http4sMUnitFunFixture: SyncIO[FunFixture[String => Resource[IO, Response[IO]]]] =
fail("This should no be called")

test("header interpolator creates a valid raw header") {
val header = ci"my-header" := "my-value"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2020-2022 Alejandro Hernández <https://github.com/alejandrohdezma>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package munit

import org.http4s.AuthedRoutes

class Http4sTestAuthedRoutesSuiteSuite extends Http4sTestAuthedRoutesSuite[String] {

test(routes = AuthedRoutes.of { case GET -> Root / "hello" as user =>
Ok(s"$user: Hi")
})(GET(uri"/hello") -> "jose").alias("Test 1") { response =>
assertIO(response.as[String], "jose: Hi")
}

test(routes = AuthedRoutes.of { case GET -> Root / "hello" / name as user =>
Ok(s"$user: Hi $name")
})(GET(uri"/hello" / "Jose").context("alex")).alias("Test 2") { response =>
assertIO(response.as[String], "alex: Hi Jose")
}

}
Loading

0 comments on commit 6f57812

Please sign in to comment.