Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit & Integration #15

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/scala/com/zeta/playthegame/AppointmentsRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package com.zeta.playthegame


import cats.effect.IO
import com.zeta.playthegame.repository.AppointmentRepository
import com.zeta.playthegame.repository.AppointmentsRepository
import io.circe.generic.auto._
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl

class AppointmentsRoutes(gameAppointmentRepository: AppointmentRepository) {
class AppointmentsRoutes(gameAppointmentRepository: AppointmentsRepository) {

def routes: HttpRoutes[IO] = {
object dsl extends Http4sDsl[IO]
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/zeta/playthegame/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.util.concurrent.Executors

import cats.effect.{ContextShift, IO, Timer}
import com.typesafe.config.ConfigFactory
import com.zeta.playthegame.repository.{AppointmentRepository, MongoConnection}
import com.zeta.playthegame.repository.{AppointmentsRepository, MongoConnection}
import org.http4s.implicits._
import org.http4s.server.blaze.BlazeServerBuilder

Expand All @@ -28,7 +28,7 @@ object Server {

implicit lazy val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(poolSize)) //Exclusive ExecutionPool for Database access only

private lazy val repository = new AppointmentRepository(MongoConnection)
private lazy val repository = new AppointmentsRepository(MongoConnection)

private lazy val httpApp = new AppointmentsRoutes(repository).routes.orNotFound

Expand Down
12 changes: 7 additions & 5 deletions src/main/scala/com/zeta/playthegame/model/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,19 @@ object Sport {
}


case class GameAppointment(appointmentId: String, authorId: String, appointmentDate: Long, createdDate: Long, game: Game) {
case class Appointment(appointmentId: String, authorId: String, appointmentDate: Long, createdDate: Long, game: Game) {

def addPlayer(user: String): GameAppointment = this.copy(game = game.addPlayer(user))
def addPlayer(user: String): Appointment = this.copy(game = game.addPlayer(user))

def getPlayers: List[String] = this.game.players

def removePlayer(user: String): GameAppointment = this.copy(game = game.removePlayer(user))
def removePlayer(user: String): Appointment = this.copy(game = game.removePlayer(user))

def changeAuthor(user: String): GameAppointment = this.copy(authorId = user)
def changeAuthor(user: String): Appointment = this.copy(authorId = user)

def addResult(aResult: Result): GameAppointment = this.copy(game = game.copy(result = Some(aResult)))
def addResult(aResult: Result): Appointment = this.copy(game = game.copy(result = Some(aResult)))

def changeAppointmentDate(newAppointmentDate: Long) = this.copy(appointmentDate = newAppointmentDate)

def sport = game.sport

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.mongodb.scala.bson.collection.immutable.Document

import scala.concurrent.ExecutionContext

class AppointmentRepository(mongoConnection: MongoConnection)(implicit executionContext: ExecutionContext) extends LoggerPerClassAware {
class AppointmentsRepository(mongoConnection: MongoConnection)(implicit executionContext: ExecutionContext) extends LoggerPerClassAware {

def getAppointmentById(id: String) = mongoConnection.appointmentsCollection map {
_.find(Document("_id" -> new ObjectId(id)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.mongodb.scala.bson.ObjectId
object Entities {

case class AppointmentDocument(_id: ObjectId, authorId: String, appointmentDate: Long, createdDate: Long, game: GameDocument) {
def toModel = GameAppointment(_id.toString, authorId, appointmentDate, createdDate, game.toModel)
def toModel = Appointment(_id.toString, authorId, appointmentDate, createdDate, game.toModel)
}

case class GameDocument(sport: String, players: List[String], result: Option[Result]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.zeta.playthegame.repository

import cats.effect.IO
import com.typesafe.config.ConfigFactory
import com.zeta.playthegame.repository.Codecs.codecRegistry
import com.zeta.playthegame.repository.Entities.AppointmentDocument
import org.mongodb.scala._

Expand All @@ -11,20 +12,22 @@ trait MongoConnection {
private lazy val mongoConfig: com.typesafe.config.Config = ConfigFactory.defaultApplication().resolve().getConfig("mongodb")
private lazy val mongoUri: String = mongoConfig.getString("uri")

private val mongoClient: IO[MongoClient] = IO {
val mongoClient: IO[MongoClient] = IO {
MongoClient(mongoUri)
}

val databaseName: String

val database: IO[MongoDatabase] = mongoClient.map(_.getDatabase(databaseName).withCodecRegistry(codecRegistry))

val appointmentsCollectionName: String
val database: IO[MongoDatabase]
val appointmentsCollection: IO[MongoCollection[AppointmentDocument]]

}

object MongoConnection extends MongoConnection {
val databaseName = "play_the_game"
val appointmentsCollectionName = "appointments"

val database: IO[MongoDatabase] = mongoClient.map(_.getDatabase(databaseName).withCodecRegistry(codecRegistry))
val appointmentsCollection: IO[MongoCollection[AppointmentDocument]] = database.map(_.getCollection(appointmentsCollectionName))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.zeta.playthegame.integration

import java.util.concurrent.TimeUnit.DAYS

import cats.effect.IO
import com.zeta.playthegame.model.Sport.FootballFive
import com.zeta.playthegame.model.{Appointment, Game}
import com.zeta.playthegame.repository.AppointmentsRepository
import com.zeta.playthegame.util.Generators
import com.zeta.playthegame.{AppointmentRequest, AppointmentsRoutes}
import io.circe.Json
import io.circe.generic.auto._
import io.circe.syntax._
import org.http4s.circe._
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.{HttpRoutes, Method, Request, Uri}
import org.scalatest.{FlatSpecLike, Matchers}

import scala.concurrent.ExecutionContext.Implicits.global

class AppointmentsIT extends FlatSpecLike
with Matchers
with Generators
with ServerApp
with MongoManager {

import RequestUtils._

private val testRepository = new AppointmentsRepository(MongoTestConnection)(global)
override val router: HttpRoutes[IO] = new AppointmentsRoutes(testRepository).routes

override def beforeAll() = {
tearDown.unsafeRunSync()
startServer
}

override def afterAll() = stopServer

"POST and Retrieve appointment" should "success" in {
val authorId = randomStringId
val createdDate = millisNow
val appointmentDate = millisNowPlus(2, DAYS)
val requestBody = AppointmentRequest(
authorId,
appointmentDate,
createdDate,
Game(FootballFive, List(randomStringId))).asJson

val postRequest = post(baseUri / "appointments", requestBody)
val maybeCreatedAppointment = response(postRequest).unsafeRunSync().as[Appointment]
maybeCreatedAppointment shouldBe 'right
val createdGameAppointment = maybeCreatedAppointment.right.get
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

horrible como se ve en github, pero bueno es la forma que sugiere la docu de ScalaTest para verificar el tipo de Either 🤷‍♂


createdGameAppointment.authorId shouldBe authorId
createdGameAppointment.appointmentDate shouldBe appointmentDate
createdGameAppointment.createdDate shouldBe createdDate
createdGameAppointment.game.sport shouldBe FootballFive

val getRequest = get(baseUri / "appointments" / createdGameAppointment.appointmentId)
val maybeRetrievedAppointment = response(getRequest).unsafeRunSync().as[Appointment]
maybeRetrievedAppointment shouldBe 'right
val retrievedGameAppointment = maybeCreatedAppointment.right.get

retrievedGameAppointment shouldBe createdGameAppointment
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Se me hace que los tests deberiamos escribirlos de forma funcional tambien.

for {
  ...
  createdAppointment <- response(postRequest)
  ...
  retrievedAppointment  <- response(getRequest)
} yield createdAppointment shouldBe retrievedAppointment

que todo eso devuelva IO[TestResult], despues agarras todos esos IO[TestResult], los metes en una lista y los corres en paralelo (.forEachPar en ZIO) en tu "TestMain".

Tambien aca tiene mas sentido NO usar either u option ademas de IO. Que el getAppointment te falle con un AppointmentNotFound hace que no tengas que maybeRetrievedAppointment shouldBe 'right y despues val retrievedGameAppointment = maybeCreatedAppointment.right.get. IO "cortocircuitea" si no te lo encontro, y listo, tu test falla. Y si lo encontró ya lo tenes, no tenes que andar preguntando si es right o left o some o none.


object RequestUtils {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esto en algun momento va a terminar siendo un DSL cross para todos los IT


def request(uri: Uri, method: Method) = Request[IO](method, uri)

def post(uri: Uri) = request(uri, Method.POST)

def post(uri: Uri, body: Json): Request[IO] = request(uri, Method.POST).withEntity(body)

def get(uri: Uri) = request(uri, Method.GET)

def response(request: Request[IO]) = BlazeClientBuilder[IO](global).resource.use(_.expect[Json](request))

def status(request: Request[IO]) = BlazeClientBuilder[IO](global).resource.use(_.status(request))
}

}
20 changes: 20 additions & 0 deletions src/test/scala/com/zeta/playthegame/integration/MongoManager.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.zeta.playthegame.integration

import cats.effect.IO
import com.zeta.playthegame.repository.Codecs.codecRegistry
import com.zeta.playthegame.repository.{Entities, MongoConnection}
import com.zeta.playthegame.util.IOExt._
import org.mongodb.scala.{MongoCollection, MongoDatabase}

trait MongoManager {
import MongoTestConnection._

def tearDown = database.map(_.drop().head()) toIO
}

object MongoTestConnection extends MongoConnection {
override val databaseName: String = "test"
val appointmentsCollectionName = "appointments"
override val database: IO[MongoDatabase] = mongoClient.map(_.getDatabase("test").withCodecRegistry(codecRegistry))
override val appointmentsCollection: IO[MongoCollection[Entities.AppointmentDocument]] = database.map(_.getCollection(appointmentsCollectionName))
}
28 changes: 28 additions & 0 deletions src/test/scala/com/zeta/playthegame/integration/ServerApp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.zeta.playthegame.integration

import cats.effect.{ContextShift, Fiber, IO, Timer}
import org.http4s.{HttpRoutes, Uri}
import org.http4s.implicits._
import org.http4s.server.blaze.BlazeServerBuilder
import org.scalatest.{BeforeAndAfterAll, TestSuite}

trait ServerApp extends BeforeAndAfterAll { self: TestSuite =>

implicit val ec = scala.concurrent.ExecutionContext.Implicits.global
implicit val cs: ContextShift[IO] = IO.contextShift(ec)
implicit val timer: Timer[IO] = IO.timer(ec)

val baseUri = Uri.unsafeFromString("http://localhost:8080")
val router: HttpRoutes[IO]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

La idea es cada IT extienda este trait y define el servicio o routes que quiera probar, y asi evitar levantar un Server con todas las rutas

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bueni pero agregame pure config.

var serverTask: Fiber[IO, Nothing] = _

private def createServer = {
BlazeServerBuilder[IO]
.bindHttp(8080, "0.0.0.0")
.withHttpApp(router.orNotFound)
}

def startServer = serverTask = createServer.resource.use(_ => IO.never).start.unsafeRunSync()

def stopServer = serverTask.cancel
}
58 changes: 58 additions & 0 deletions src/test/scala/com/zeta/playthegame/model/GameSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.zeta.playthegame.model

import java.util.concurrent.TimeUnit.DAYS

import com.zeta.playthegame.model.Sport.{FootballFive, TennisSingle}
import com.zeta.playthegame.util.Generators
import org.scalatest.{FlatSpecLike, Matchers}

class GameSpec extends FlatSpecLike with Matchers with Generators {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No veo que uses ningun Generator aca. Tampoco veo que en ESTE pr hayas agregado el archivocom.zeta.playthegame.util.Generators como para ver de que se trata.


val zeta = randomStringId
val palan = randomStringId
val footballFive = Game(FootballFive, List(zeta))
val appointment = Appointment(randomStringId, zeta, millisNowPlus(2, DAYS), millisNow, footballFive)

it should "Add Players to appointment" in {
appointment.getPlayers.size shouldBe 1
val updatedAppointment = appointment.addPlayer(palan)
updatedAppointment.getPlayers.size shouldBe 2
updatedAppointment.getPlayers should contain theSameElementsAs List(zeta, palan)
}

it should "Delete Players of appointment" in {
val updatedAppointment = appointment.removePlayer(zeta)
updatedAppointment.getPlayers.size shouldBe 0
}

it should "Change author of appointment" in {
val palan = randomStringId
appointment.authorId shouldBe zeta
val updatedAppointment = appointment.changeAuthor(palan)
updatedAppointment.authorId shouldBe palan
}

it should "Change appointment date" in {
val updated = appointment.changeAppointmentDate(millisNowPlus(4, DAYS))
updated.appointmentDate should be > appointment.appointmentDate
}

it should "Change Sport" in {
val updated = appointment.changeSport(TennisSingle)
updated.sport shouldBe TennisSingle
}

it should "Set a result" in {
val updated = appointment.addPlayer(palan)
.changeSport(TennisSingle)
.addResult(Result(List(palan), List(zeta), 3, 2))

updated.winner shouldBe 'defined
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sameeeeeeee

updated.loser shouldBe 'defined

updated.winner.get should contain theSameElementsAs List(palan)
updated.loser.get should contain theSameElementsAs List(zeta)

}

}