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

Creating a mock RedisClient #109

Open
patrickcunningham opened this issue Feb 24, 2015 · 6 comments
Open

Creating a mock RedisClient #109

patrickcunningham opened this issue Feb 24, 2015 · 6 comments

Comments

@patrickcunningham
Copy link
Contributor

Hi,
This library is great and hoping to put it into full use shortly. However I am using behind a Factory pattern to abstract away the cache implementation (so we can easily swap in out). I was looking at testing without having a local version of redis running. Testing this is difficult and I would like to create a MockRedisClient that I can use in my factory, maybe that would look something like this:

class MockRedisClient(clientRef: ActorRef) extends RedisClient(clientRef) {
  val mmap = collection.mutable.Map[String, Any]()

  override def get[A](key: String)(implicit timeout: Timeout, reader: Reader[A]): Future[Option[A]] = {
    val value = mmap.get(key)
    Future.successful(value.asInstanceOf[Option[A]])
  }


  override def set(key: String, value: Stringified, exORpx: Option[SetExpiryOption], nxORxx: Option[SetConditionOption])(implicit timeout: Timeout): Future[Boolean] = {
    mmap.put(key, value).
    Future.successful(true)
  }


  override def setex(key: String, expiry: Int, value: Stringified)(implicit timeout: Timeout): Future[Boolean] = {
    mmap.put(key, value)
    Future.successful(true)
  }
}

This is not possible on the get method due to the fact that com.redis.serialization.Reader is private to package com.redis. I was wondering if this was an intended restriction and if this visibility is something you would consider removing so that we could Mock the RedisClient easier? I've tried it on a local copy and is then accessible to to override.

An alternative is to create a local com.redis package and create a MockRedisClient class which is a hacky

@debasishg
Copy link
Owner

May be you can try using an algebra based approach as in https://github.com/ethul/redis-algebra. This is pure algebra without any implementation. The implementation is delegated to an interpreter, which uses scala-redis-nb underneath. It's based on the approach of free monads. This also lets you swap out actual implementation with a fake one, just like the one you need.

@guersam
Copy link
Collaborator

guersam commented Mar 1, 2015

BTW, although our intention was to hide as many as interfaces to keep the API simple, I think it wouldn't be that harmful to disclose Reader, Writer and their children because they are already well-namespaced. A minor concern is that their names sound common so that it might cause collision with Scalaz typeclasses when both are imported using wildcard, but perhaps I've gone too far..

@patrickcunningham
Copy link
Contributor Author

Hi,
Thank you for your comments and sorry its taken a while to get back to you.
Regarding the redis-algebra solution, I'm still getting to grips with scala and chose this library for its excellent integration with the Spray json libraries, would I still be able to map objects to json and store in redis with this solution?

Please see below for another rough sketch implementation where making Reader accessible from outside com.redis would be valuable. Harnessing the power/elegance of this library to store JSON in Redis and map back to objects

object CacheFactory {

  implicit val timeout = Timeout(1.seconds)

  def apply()(implicit refFactory: ActorRefFactory): Cache = {

    val config = ConfigFactory.load().getConfig("cache")

    if (config.getString("client") == "redis") {
      try {
        val actor = Await.result(refFactory.actorSelection("/user/redis-client").resolveOne(), timeout.duration)
        new RedisCache(new RedisClient(actor))
      } catch {
        case ex: Exception => new PsuedoCache
      }
    } else {
      new PsuedoCache
    }
  }
}

trait Cache {
  def get[A](key:String)(implicit timeout: Timeout, reader: Reader[A]): Future[Option[A]]
}

class RedisCache(redis: RedisClient) extends Cache {
  /**
   * Retrieve cache entry by key
   *
   * @param key
   * @return Future[Option[String]
   */
  def get[A](key:String): Future[Option[A]] = {
    redis.get[A](key)
  }
}

Retrieving an object like so:

case class Person(id: Int, name: String)
implicit val personFormat = jsonFormat2(Person)
CacheFactory().get[Person]("objectKey")

Do you think this is a good solution? Am trying to hide the cache implementation from the consumer. I have tested this only using worksheets with some success.

Patrick

@patrickcunningham
Copy link
Contributor Author

Hi,
I'd like to submit a pull request for this change. I think the above is an elegant solution. Obviously feel free to reject but just giving you a heads up

Thanks,
Patrick

@debasishg
Copy link
Owner

Thanks a lot .. I will take a look over the weekend. Sorry for being late .. quite flooded at the moment ..

debasishg referenced this issue in patrickcunningham/scala-redis-nb Apr 11, 2015
@mateusduboli
Copy link

mateusduboli commented Nov 10, 2016

Also regarding this issue I had some issues mocking the RedisClient as well.

My current test setup uses ScalaMock, the main issue I fell with trying to implement mocks is that the RedisClient is a class without a empty constructor and ScalaMock is not good with mocking concrete objects.

The way ScalaMock works best it's at mocking traits and RedisOps is package private to com.redis (private[com.redis]).

I had some trouble doing the same thing on the debasishg/scala-redis when I had to mock it's RedisClient. In that case I was able to mock RedisCommand which was great, but since I had to use RedisPool and the withClient function only worked with the concrete RedisClient as the inner block's argument.

I strongly believe that being compatible with ScalaMock is a trait (no pun intended) this project could have :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants