package nick import java.time.Instant import java.util.concurrent.atomic.AtomicInteger import scala.util.{Failure, Random, Success, Try} case class Dto(name: String) case class Entity( var id: Int, var name: String, var createdAt: Instant ) // like true Dao sometimes fails class Dao { val random = new Random() var currentId = new AtomicInteger(0) def store(e: Entity): Unit = { if (random.nextInt(10) < 1) { throw new Exception("Database error") } else { e.id = currentId.incrementAndGet() } } } class MyService { val dao = new Dao() // this method requires to mock dao to be tested def createMetadataBefore(dto: Dto): Dto = { val e = convertD2E(dto) e.createdAt = Instant.now() dao.store(e) convertE2D(e) } // split the effectful method into 2 pure functions // one is before store call def createMetadata1(dto: Dto, storeFun: Entity ⇒ Try[Entity]): Try[Entity] = { val e = convertD2E(dto) e.createdAt = Instant.now() storeFun(e) } // one is after store call def createMetadata2(e: Entity): Dto = { convertE2D(e) } def createMetadataAfter(dto: Dto): Dto = { val storeFun: Entity ⇒ Try[Entity] = e ⇒ { try { val e1 = e.copy() // to not spoil the argument dao.store(e1) Success(e1) } catch { case x: Exception ⇒ Failure(x) } } val result = createMetadata1(dto, storeFun) result match { case Success(e) ⇒ createMetadata2(e) case Failure(ex) ⇒ throw ex } } def convertE2D(e: Entity): Dto = Dto(e.name) def convertD2E(d: Dto): Entity = Entity(0, d.name, Instant.MIN) } object MyServiceTest extends App { val service = new MyService def test(): Unit = { val now = Instant.now() val storingFun: Entity ⇒ Try[Entity] = e ⇒ Success(e.copy(id = 1, createdAt = now)) val dto = Dto("Hello!") val r1 = service.createMetadata1(dto, storingFun) assert(r1 == Success(Entity(1, "Hello!", now))) val r2 = service.createMetadata2(r1.get) assert(r2 == Dto("Hello!")) } test() }