问题
In my play framework (2.5) app, I need to write unit tests for services.
I need to isolate data access logic to able to test service layer in isolation, for this I want to create repository interfaces and MOCK them in my unit tests:
class UserService {
def signUpNewUser(username: String, memberName: String): Future[Unit] {
val userId = 1 // Set to 1 for demo
val user = User(userId, username)
val member = Member(memberName, userId)
// ---- I NEED TO EXECUTE THIS BLOCK WITHIN TRANSACTION ----
for {
userResult <- userRepository.save(user)
memberRepository.save(member)
} yield ()
// ---- END OF TRANSACTION ----
}
}
In the above example, userRepository.save(User)
and memberRepository.save(member)
operations should be performed within transaction.
I don't want to use slick directly in my service layer because it will complicate my tests.
Also, I don't want to use embedded database for my unit tests, elsewhere it would be a NOT unit test, I need complete isolation.
I do not want my repository interfaces to be depended on slick at all, but need something like this:
trait UserRepository {
findById(id: Long): Future[Option[User]]
save(user: User): Future[Unit]
}
how can I achieve this with slick?
回答1:
OK - let's decompose your question into three parts.
How to execute block in transaction
Basically read this answer: How to use transaction in slick
As soon as you convert DBIO
to Future
you are done. No chances to compose multiple operation within single transaction. End of story.
How to avoid using Slick
in tests
This is basically a design question - if you want to have a business layer on top of Repository
/ DAO
/ whatever - than let this service layer deal with transactions. You won't need to interact with Slick
outside of this layer.
Avoiding your repository interfaces to depend on Slick
In most straightforward way - you need to depend on Slick DBIO
to compose operations within transaction (and composing Repository
methods within transaction is something that you cannot avoid in any serious application).
If you want to avoid depending on DBIO
you would perhaps create you own monadic type, say TransactionBoundary[T]
or TransactionContext[T]
.
Then you would have something like TransactionManager
that would execute this TransactionContext[T]
.
IMHO not worth the effort, I'd simply use DBIO
which has a brilliant name already (like Haskell's IO
monad - DBIO
informs you that you have a description of IO
operations performed on your storage). But let's assume that you still want to avoid it.
You could do something like that perhaps:
package transaction {
object Transactions {
implicit class TransactionBoundary[T](private[transaction] val dbio: DBIO[T]) {
// ...
}
}
class TransactionManager {
def execute[T](boundary: TransactionBoundary[T]): Future[T] = db.run(boundary.dbio)
}
}
Your trait would look like this:
trait UserRepository {
findById(id: Long): TransactionBoundary[Option[User]]
save(user: User): TransactionBoundary[Unit]
}
and somewhere in your code you would do like this:
transactionManager.execute(
for {
userResult <- userRepository.save(user)
memberRepository.save(member)
} yield ()
)
By using implicit conversion you would have your results of methods in Repository
be automatically converted to your TransactionBoundary
.
But again - IMHO all of the above doesn't bring any actual advantage over using DBIO
(except perhaps taste of esthetics). If you want to avoid using Slick
related classes outside of certain layer, just make a type alias like this:
type TransactionBoundary[T] = DBIO[T]
and use it everywhere.
来源:https://stackoverflow.com/questions/41618176/slick-3-how-to-implement-repository-pattern-with-transactions