Skip to content

Commit 2e20857

Browse files
authored
Setup testing (#13)
* WIP: add acolyte dependency * Add use case + specs for SqlQueryRunner * Formatting code + test * Refactor tests with specs2 matchers + update build.sbt * Add trait for companion objects functions * Add combinator for asynchronous effects * Removing type alias companions * Add a ComposeWithCompletion to handle async structures * Refactor: cleaning * Formatting
1 parent ac1589d commit 2e20857

File tree

13 files changed

+402
-23
lines changed

13 files changed

+402
-23
lines changed

build.sbt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ val commonSettings = Seq(
4141
scalacOptions in (Test, compile) ~= (_.filterNot(
4242
Set(
4343
"-Ywarn-unused:imports",
44-
"-Xfatal-warnings"
44+
"-Xfatal-warnings",
45+
"-Yrangepos"
4546
))),
4647
resolvers ++= Seq[Resolver](
4748
Resolver.sonatypeRepo("releases")
@@ -60,7 +61,10 @@ lazy val core = (project in file("core"))
6061
.settings(
6162
name := "query-core",
6263
libraryDependencies ++= Seq(
63-
Dependencies.cats
64+
Dependencies.acolyte % Test,
65+
Dependencies.anorm % Test,
66+
Dependencies.cats,
67+
Dependencies.specs2 % Test
6468
)
6569
)
6670

@@ -72,8 +76,7 @@ lazy val sampleAppExample = (project in file("examples/sample-app"))
7276
libraryDependencies ++= Seq(
7377
jdbc,
7478
Dependencies.anorm,
75-
Dependencies.h2,
76-
Dependencies.scalaTestPlusPlay
79+
Dependencies.h2
7780
)
7881
)
7982
.dependsOn(core)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.zengularity.querymonad.core.database
2+
3+
import scala.concurrent.{ExecutionContext, Future}
4+
import scala.language.higherKinds
5+
6+
/**
7+
* Heavily inspired from work done by @cchantep in Acolyte (see acolyte.reactivemongo.ComposeWithCompletion)
8+
*/
9+
trait ComposeWithCompletion[F[_], Out] {
10+
type Outer <: Future[_]
11+
12+
def apply[In](resource: In, f: In => F[Out])(onComplete: In => Unit)(
13+
implicit ec: ExecutionContext): Outer
14+
}
15+
16+
object ComposeWithCompletion extends LowPriorityCompose {
17+
18+
type Aux[F[_], A, B] = ComposeWithCompletion[F, A] { type Outer = Future[B] }
19+
20+
implicit def futureOut[A]: Aux[Future, A, A] =
21+
new ComposeWithCompletion[Future, A] {
22+
type Outer = Future[A]
23+
24+
def apply[In](resource: In, f: In => Future[A])(onComplete: In => Unit)(
25+
implicit ec: ExecutionContext): Outer =
26+
f(resource).andThen {
27+
case _ => onComplete(resource)
28+
}
29+
30+
override val toString = "futureOut"
31+
}
32+
33+
}
34+
35+
trait LowPriorityCompose { _: ComposeWithCompletion.type =>
36+
37+
implicit def pureOut[F[_], A]: Aux[F, A, F[A]] =
38+
new ComposeWithCompletion[F, A] {
39+
type Outer = Future[F[A]]
40+
41+
def apply[In](resource: In, f: In => F[A])(onComplete: In => Unit)(
42+
implicit ec: ExecutionContext): Outer =
43+
Future(f(resource)).andThen { case _ => onComplete(resource) }
44+
45+
override val toString = "pureOut"
46+
}
47+
48+
}
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
11
package com.zengularity.querymonad.core.database
22

3-
import scala.concurrent.{ExecutionContext, Future}
3+
import scala.concurrent.ExecutionContext
4+
import scala.language.higherKinds
45

56
/**
67
* A class who can run a Query.
78
*/
89
sealed trait QueryRunner[Resource] {
9-
def apply[T](query: Query[Resource, T]): Future[T]
10+
def apply[M[_], T](
11+
query: QueryT[M, Resource, T]
12+
)(
13+
implicit compose: ComposeWithCompletion[M, T]
14+
): compose.Outer
1015
}
1116

1217
object QueryRunner {
13-
private class DefaultRunner[Resource](wr: WithResource[Resource])(
14-
implicit ec: ExecutionContext)
15-
extends QueryRunner[Resource] {
16-
def apply[T](query: Query[Resource, T]): Future[T] =
17-
Future(wr(query.run))
18+
private class DefaultRunner[Resource](
19+
wr: WithResource[Resource]
20+
)(
21+
implicit ec: ExecutionContext
22+
) extends QueryRunner[Resource] {
23+
24+
def apply[M[_], T](
25+
query: QueryT[M, Resource, T]
26+
)(
27+
implicit compose: ComposeWithCompletion[M, T]
28+
): compose.Outer = {
29+
wr { resource =>
30+
compose(resource, query.run)(wr.releaseIfNecessary)
31+
}
32+
}
33+
1834
}
1935

2036
// Default factory
21-
def apply[Resource](wr: WithResource[Resource])(
22-
implicit ec: ExecutionContext): QueryRunner[Resource] =
37+
def apply[Resource](
38+
wr: WithResource[Resource]
39+
)(
40+
implicit ec: ExecutionContext
41+
): QueryRunner[Resource] =
2342
new DefaultRunner(wr)
2443
}

core/src/main/scala/core/database/WithResource.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ package com.zengularity.querymonad.core.database
22

33
trait WithResource[Resource] {
44
def apply[A](f: Resource => A): A
5+
6+
def releaseIfNecessary(resource: Resource): Unit
57
}

core/src/main/scala/core/database/package.scala

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.zengularity.querymonad.core
22

33
import scala.language.higherKinds
44

5-
import cats.{Applicative, Id}
5+
import cats.Applicative
6+
7+
import cats.Id
68
import cats.data.{Reader, ReaderT}
79

810
package object database {
@@ -19,16 +21,28 @@ package object database {
1921
type QueryT[F[_], Resource, A] = ReaderT[F, Resource, A]
2022

2123
object QueryT {
24+
def apply[M[_], Resource, A](
25+
run: Resource => M[A]
26+
): QueryT[M, Resource, A] =
27+
new QueryT(run)
28+
2229
def pure[M[_]: Applicative, Resource, A](a: A) =
2330
ReaderT.pure[M, Resource, A](a)
2431

25-
def ask[M[_]: Applicative, Resource] = ReaderT.ask[M, Resource]
32+
def ask[M[_]: Applicative, Resource] =
33+
ReaderT.ask[M, Resource]
34+
35+
def liftF[M[_], Resource, A](ma: M[A]) =
36+
ReaderT.liftF[M, Resource, A](ma)
2637

27-
def liftF[M[_], Resource, A](ma: M[A]) = ReaderT.liftF[M, Resource, A](ma)
38+
def fromQuery[M[_], Resource, A](
39+
query: Query[Resource, M[A]]
40+
): QueryT[M, Resource, A] =
41+
QueryT[M, Resource, A](query.run)
2842
}
2943

3044
type QueryO[Resource, A] = QueryT[Option, Resource, A]
3145

32-
type QueryE[Resource, A, Err] =
46+
type QueryE[Resource, Err, A] =
3347
QueryT[({ type F[T] = Either[Err, T] })#F, Resource, A]
3448
}

core/src/main/scala/core/module/sql/package.scala

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ package com.zengularity.querymonad.core.module
33
import java.sql.Connection
44

55
import scala.concurrent.ExecutionContext
6+
import scala.language.higherKinds
7+
8+
import cats.Applicative
69

710
import com.zengularity.querymonad.core.database.{
811
Query,
912
QueryRunner,
13+
QueryT,
14+
QueryO,
15+
QueryE,
1016
WithResource
1117
}
1218

1319
package object sql {
20+
21+
// Query aliases
1422
type SqlQuery[A] = Query[Connection, A]
1523

1624
object SqlQuery {
@@ -21,6 +29,29 @@ package object sql {
2129
def apply[A](f: Connection => A) = new SqlQuery(f)
2230
}
2331

32+
// Query transformer aliases
33+
type SqlQueryT[F[_], A] = QueryT[F, Connection, A]
34+
35+
object SqlQueryT {
36+
def apply[M[_], A](run: Connection => M[A]) =
37+
QueryT.apply[M, Connection, A](run)
38+
39+
def pure[M[_]: Applicative, A](a: A) =
40+
QueryT.pure[M, Connection, A](a)
41+
42+
def ask[M[_]: Applicative] = QueryT.ask[M, Connection]
43+
44+
def liftF[M[_], A](ma: M[A]) = QueryT.liftF[M, Connection, A](ma)
45+
46+
def fromQuery[M[_], A](query: SqlQuery[M[A]]) =
47+
QueryT.fromQuery[M, Connection, A](query)
48+
}
49+
50+
type SqlQueryO[A] = QueryO[Connection, A]
51+
52+
type SqlQueryE[A, Err] = QueryE[Connection, A, Err]
53+
54+
// Query runner aliases
2455
type WithSqlConnection = WithResource[Connection]
2556

2657
type SqlQueryRunner = QueryRunner[Connection]
@@ -30,4 +61,5 @@ package object sql {
3061
implicit ec: ExecutionContext): SqlQueryRunner =
3162
QueryRunner[Connection](wc)
3263
}
64+
3365
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.zengularity.querymonad.test.core.module.sql
2+
3+
import acolyte.jdbc.{
4+
AcolyteDSL,
5+
ExecutedParameter,
6+
QueryExecution,
7+
QueryResult => AcolyteQueryResult
8+
}
9+
import org.specs2.concurrent.ExecutionEnv
10+
import org.specs2.mutable.Specification
11+
12+
import com.zengularity.querymonad.core.module.sql.{
13+
SqlQuery,
14+
SqlQueryT,
15+
SqlQueryRunner,
16+
WithSqlConnection
17+
}
18+
import com.zengularity.querymonad.test.core.module.sql.models.{
19+
Material,
20+
Professor
21+
}
22+
import com.zengularity.querymonad.test.core.module.sql.utils.SqlConnectionFactory
23+
24+
class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification {
25+
26+
"SqlQueryRunner" should {
27+
// execute lift Queries
28+
"return integer value lift in Query using pure" in {
29+
val withSqlConnection: WithSqlConnection =
30+
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
31+
val runner = SqlQueryRunner(withSqlConnection)
32+
val query = SqlQuery.pure(1)
33+
34+
runner(query) aka "material" must beTypedEqualTo(1).await
35+
}
36+
37+
"return optional value lift in Query using liftF" in {
38+
val withSqlConnection: WithSqlConnection =
39+
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
40+
val runner = SqlQueryRunner(withSqlConnection)
41+
val query = SqlQueryT.liftF(Seq(1))
42+
43+
runner(query) aka "material" must beTypedEqualTo(Seq(1)).await
44+
}
45+
46+
// execute single query
47+
"retrieve professor with id 1" in {
48+
val withSqlConnection: WithSqlConnection =
49+
SqlConnectionFactory.withSqlConnection(Professor.resultSet)
50+
val runner = SqlQueryRunner(withSqlConnection)
51+
val result = runner(Professor.fetchProfessor(1)).map(_.get)
52+
53+
result aka "professor" must beTypedEqualTo(
54+
Professor(1, "John Doe", 35, 1)).await
55+
}
56+
57+
"retrieve material with id 1" in {
58+
val withSqlConnection: WithSqlConnection =
59+
SqlConnectionFactory.withSqlConnection(Material.resultSet)
60+
val runner = SqlQueryRunner(withSqlConnection)
61+
val result = runner(Material.fetchMaterial(1)).map(_.get)
62+
63+
result aka "material" must beTypedEqualTo(
64+
Material(1, "Computer Science", 20, "Beginner")).await
65+
}
66+
67+
"not retrieve professor with id 2" in {
68+
val withSqlConnection: WithSqlConnection =
69+
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
70+
val runner = SqlQueryRunner(withSqlConnection)
71+
val query = for {
72+
_ <- SqlQuery.ask
73+
professor <- Professor.fetchProfessor(2)
74+
} yield professor
75+
76+
runner(query) aka "material" must beNone.await
77+
}
78+
79+
// execute composed queries into a single transaction
80+
"retrieve professor with id 1 and his material" in {
81+
val handler = AcolyteDSL.handleQuery {
82+
case QueryExecution("SELECT * FROM professors where id = ?",
83+
ExecutedParameter(1) :: Nil) =>
84+
Professor.resultSet
85+
case QueryExecution("SELECT * FROM materials where id = ?",
86+
ExecutedParameter(1) :: Nil) =>
87+
Material.resultSet
88+
case _ =>
89+
AcolyteQueryResult.Nil
90+
}
91+
val withSqlConnection: WithSqlConnection =
92+
SqlConnectionFactory.withSqlConnection(handler)
93+
val runner = SqlQueryRunner(withSqlConnection)
94+
val query = for {
95+
professor <- Professor.fetchProfessor(1).map(_.get)
96+
material <- Material.fetchMaterial(professor.material).map(_.get)
97+
} yield (professor, material)
98+
99+
runner(query) aka "professor and material" must beTypedEqualTo(
100+
Tuple2(Professor(1, "John Doe", 35, 1),
101+
Material(1, "Computer Science", 20, "Beginner"))).await
102+
}
103+
104+
"not retrieve professor with id 1 and no material" in {
105+
import cats.instances.option._
106+
val handler = AcolyteDSL.handleQuery {
107+
case QueryExecution("SELECT * FROM professors where id = {id}", _) =>
108+
Professor.resultSet
109+
case _ =>
110+
AcolyteQueryResult.Nil
111+
}
112+
val withSqlConnection: WithSqlConnection =
113+
SqlConnectionFactory.withSqlConnection(handler)
114+
val runner = SqlQueryRunner(withSqlConnection)
115+
val query = for {
116+
professor <- SqlQueryT.fromQuery(Professor.fetchProfessor(1))
117+
material <- SqlQueryT.fromQuery(
118+
Material.fetchMaterial(professor.material))
119+
} yield (professor, material)
120+
121+
runner(query) aka "professor and material" must beNone.await
122+
}
123+
124+
// execute async queries
125+
"retrieve int value fetch in an async context" in {
126+
import scala.concurrent.Future
127+
import anorm.{SQL, SqlParser}
128+
import acolyte.jdbc.RowLists
129+
import acolyte.jdbc.Implicits._
130+
val queryResult: AcolyteQueryResult =
131+
(RowLists.rowList1(classOf[Int] -> "res").append(5))
132+
val withSqlConnection: WithSqlConnection =
133+
SqlConnectionFactory.withSqlConnection(queryResult)
134+
val runner = SqlQueryRunner(withSqlConnection)
135+
val query =
136+
SqlQueryT { implicit connection =>
137+
Future {
138+
Thread.sleep(900) // to simulate a slow-down
139+
SQL("SELECT 5 as res")
140+
.as(SqlParser.int("res").single)
141+
}
142+
}
143+
144+
runner(query) aka "result" must beTypedEqualTo(5).await
145+
}
146+
}
147+
148+
}

0 commit comments

Comments
 (0)