[DRE-commits] [ruby-fakeredis] 01/02: Imported Upstream version 0.5.0

Abhijith PA abhijithpa-guest at moszumanska.debian.org
Wed Nov 4 06:22:13 UTC 2015


This is an automated email from the git hooks/post-receive script.

abhijithpa-guest pushed a commit to branch master
in repository ruby-fakeredis.

commit b088fb1a427b7e7fea55101914b45a41f6a7ad68
Author: Abhijith PA <abhijith at openmailbox.org>
Date:   Wed Nov 4 10:32:10 2015 +0530

    Imported Upstream version 0.5.0
---
 .gitignore                                   |    6 +
 .travis.yml                                  |    8 +
 Gemfile                                      |   13 +
 LICENSE                                      |   21 +
 README.md                                    |   86 +++
 Rakefile                                     |   27 +
 fakeredis.gemspec                            |   23 +
 lib/fake_redis.rb                            |    1 +
 lib/fakeredis.rb                             |    6 +
 lib/fakeredis/command_executor.rb            |   25 +
 lib/fakeredis/expiring_hash.rb               |   70 ++
 lib/fakeredis/rspec.rb                       |   23 +
 lib/fakeredis/sort_method.rb                 |  116 +++
 lib/fakeredis/sorted_set_argument_handler.rb |   74 ++
 lib/fakeredis/sorted_set_store.rb            |   80 ++
 lib/fakeredis/transaction_commands.rb        |   83 +++
 lib/fakeredis/version.rb                     |    3 +
 lib/fakeredis/zset.rb                        |   33 +
 lib/redis/connection/memory.rb               | 1034 ++++++++++++++++++++++++++
 metadata.yml                                 |  124 +++
 spec/compatibility_spec.rb                   |    9 +
 spec/connection_spec.rb                      |   85 +++
 spec/hashes_spec.rb                          |  195 +++++
 spec/keys_spec.rb                            |  371 +++++++++
 spec/lists_spec.rb                           |  203 +++++
 spec/memory_spec.rb                          |   28 +
 spec/server_spec.rb                          |  100 +++
 spec/sets_spec.rb                            |  269 +++++++
 spec/sort_method_spec.rb                     |   68 ++
 spec/sorted_sets_spec.rb                     |  440 +++++++++++
 spec/spec_helper.rb                          |   28 +
 spec/spec_helper_live_redis.rb               |   14 +
 spec/strings_spec.rb                         |  284 +++++++
 spec/support/shared_examples/sortable.rb     |   69 ++
 spec/transactions_spec.rb                    |   84 +++
 spec/upcase_method_name_spec.rb              |   18 +
 36 files changed, 4121 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..898cd4c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+.rvmrc
+*.rbc
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..50cb337
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: ruby
+rvm:
+  - 1.9.2
+  - 1.9.3
+  - 2.0.0
+  - 2.1.1
+  - jruby-19mode
+  - rbx-2
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..510e05a
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,13 @@
+source "https://rubygems.org"
+
+gem 'rake'
+gem 'rdoc'
+
+platforms :rbx do
+  gem 'racc'
+  gem 'rubysl', '~> 2.0'
+  gem 'psych'
+end
+
+# Specify your gem's dependencies in fakeredis.gemspec
+gemspec
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..89057cc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2011-2014 Guillermo Iguaran
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e296d36
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# FakeRedis [![Build Status](https://secure.travis-ci.org/guilleiguaran/fakeredis.png)](http://travis-ci.org/guilleiguaran/fakeredis)
+This a fake implementation of redis-rb for machines without Redis or test environments
+
+
+## Installation
+
+Install the gem:
+
+    gem install fakeredis
+
+Add it to your Gemfile:
+
+    gem "fakeredis"
+
+
+## Versions
+
+FakeRedis currently supports redis-rb v3.x.y or later, if you are using
+redis-rb v2.2.x install the version 0.3.x:
+
+    gem install fakeredis -v "~> 0.3.0"
+
+or use the branch 0-3-x on your Gemfile:
+
+    gem "fakeredis", :git => "git://github.com/guilleiguaran/fakeredis.git", :branch => "0-3-x"
+
+
+## Usage
+
+You can use FakeRedis without any changes:
+
+    require "fakeredis"
+    
+    redis = Redis.new
+    
+    >> redis.set "foo", "bar"
+    => "OK"
+    
+    >> redis.get "foo"
+    => "bar"
+
+Read [redis-rb](https://github.com/ezmobius/redis-rb) documentation and
+[Redis](http://redis.io) homepage for more info about commands
+
+## Usage with RSpec
+
+Require this either in your Gemfile or in RSpec's support scripts. So either: 
+
+    # Gemfile
+    group :test do
+      gem "rspec"
+      gem "fakeredis", :require => "fakeredis/rspec"
+    end
+
+Or:
+
+    # spec/support/fakeredis.rb
+    require 'fakeredis/rspec'
+
+## Acknowledgements
+
+* [dim](https://github.com/dim)
+* [czarneckid](https://github.com/czarneckid)
+* [obrie](https://github.com/obrie)
+* [jredville](https://github.com/jredville)
+* [redsquirrel](https://github.com/redsquirrel)
+* [dpick](https://github.com/dpick)
+* [caius](https://github.com/caius) 
+* [Travis-CI](http://travis-ci.org/) (Travis-CI also uses Fakeredis in its tests!!!)
+
+
+## Contributing to FakeRedis
+
+* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
+* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
+* Fork the project
+* Start a feature/bugfix branch
+* Commit and push until you are happy with your contribution
+* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
+* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
+
+
+## Copyright
+
+Copyright (c) 2011-2014 Guillermo Iguaran. See LICENSE for
+further details.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..5ff0ad6
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,27 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+$:.push File.expand_path("../lib", __FILE__)
+require "fakeredis/version"
+
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core'
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec) do |spec|
+  spec.pattern = FileList['spec/**/*_spec.rb']
+  spec.ruby_opts="-w"
+end
+
+task :default => :spec
+
+require 'rdoc/task'
+Rake::RDocTask.new do |rdoc|
+  version = FakeRedis::VERSION
+
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title = "fakeredis #{version}"
+  rdoc.rdoc_files.include('README*')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
diff --git a/fakeredis.gemspec b/fakeredis.gemspec
new file mode 100644
index 0000000..04b4d25
--- /dev/null
+++ b/fakeredis.gemspec
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "fakeredis/version"
+
+Gem::Specification.new do |s|
+  s.name        = "fakeredis"
+  s.version     = FakeRedis::VERSION
+  s.platform    = Gem::Platform::RUBY
+  s.authors     = ["Guillermo Iguaran"]
+  s.email       = ["guilleiguaran at gmail.com"]
+  s.homepage    = "https://guilleiguaran.github.com/fakeredis"
+  s.license     = "MIT"
+  s.summary     = %q{Fake (In-memory) driver for redis-rb.}
+  s.description = %q{Fake (In-memory) driver for redis-rb. Useful for testing environment and machines without Redis.}
+
+  s.files         = `git ls-files`.split("\n")
+  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
+  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+  s.require_paths = ["lib"]
+
+  s.add_runtime_dependency(%q<redis>, ["~> 3.0"])
+  s.add_development_dependency(%q<rspec>, ["~> 3.0"])
+end
diff --git a/lib/fake_redis.rb b/lib/fake_redis.rb
new file mode 100644
index 0000000..b15dd6a
--- /dev/null
+++ b/lib/fake_redis.rb
@@ -0,0 +1 @@
+require "fakeredis"
diff --git a/lib/fakeredis.rb b/lib/fakeredis.rb
new file mode 100644
index 0000000..255ba63
--- /dev/null
+++ b/lib/fakeredis.rb
@@ -0,0 +1,6 @@
+require 'redis'
+require 'redis/connection/memory'
+
+module FakeRedis
+  Redis = ::Redis
+end
diff --git a/lib/fakeredis/command_executor.rb b/lib/fakeredis/command_executor.rb
new file mode 100644
index 0000000..8401951
--- /dev/null
+++ b/lib/fakeredis/command_executor.rb
@@ -0,0 +1,25 @@
+module FakeRedis
+  module CommandExecutor
+    def write(command)
+      meffod = command.shift.to_s.downcase.to_sym
+
+      if in_multi && !(TRANSACTION_COMMANDS.include? meffod) # queue commands
+        queued_commands << [meffod, *command]
+        reply = 'QUEUED'
+      elsif respond_to?(meffod)
+        reply = send(meffod, *command)
+      else
+        raise Redis::CommandError, "ERR unknown command '#{meffod}'"
+      end
+
+      if reply == true
+        reply = 1
+      elsif reply == false
+        reply = 0
+      end
+
+      replies << reply
+      nil
+    end
+  end
+end
diff --git a/lib/fakeredis/expiring_hash.rb b/lib/fakeredis/expiring_hash.rb
new file mode 100644
index 0000000..aefbdd7
--- /dev/null
+++ b/lib/fakeredis/expiring_hash.rb
@@ -0,0 +1,70 @@
+module FakeRedis
+  # Represents a normal hash with some additional expiration information
+  # associated with each key
+  class ExpiringHash < Hash
+    attr_reader :expires
+
+    def initialize(*)
+      super
+      @expires = {}
+    end
+
+    def [](key)
+      key = normalize key
+      delete(key) if expired?(key)
+      super
+    end
+
+    def []=(key, val)
+      key = normalize key
+      expire(key)
+      super
+    end
+
+    def delete(key)
+      key = normalize key
+      expire(key)
+      super
+    end
+
+    def expire(key)
+      key = normalize key
+      expires.delete(key)
+    end
+
+    def expired?(key)
+      key = normalize key
+      expires.include?(key) && expires[key] < Time.now
+    end
+
+    def key?(key)
+      key = normalize key
+      delete(key) if expired?(key)
+      super
+    end
+
+    def values_at(*keys)
+      keys.each do |key|
+        key = normalize(key)
+        delete(key) if expired?(key)
+      end
+      super
+    end
+
+    def keys
+      super.select do |key|
+        key = normalize(key)
+        if expired?(key)
+          delete(key)
+          false
+        else
+          true
+        end
+      end
+    end
+
+    def normalize key
+      key.to_s
+    end
+  end
+end
diff --git a/lib/fakeredis/rspec.rb b/lib/fakeredis/rspec.rb
new file mode 100644
index 0000000..3a99d2b
--- /dev/null
+++ b/lib/fakeredis/rspec.rb
@@ -0,0 +1,23 @@
+# Require this either in your Gemfile or in RSpec's
+# support scripts. Examples:
+#
+#   # Gemfile
+#   group :test do
+#     gem "rspec"
+#     gem "fakeredis", :require => "fakeredis/rspec"
+#   end
+#
+#   # spec/support/fakeredis.rb
+#   require 'fakeredis/rspec'
+#
+
+require 'rspec/core'
+require 'fakeredis'
+
+RSpec.configure do |c|
+
+  c.before do    
+    Redis::Connection::Memory.reset_all_databases
+  end
+
+end
diff --git a/lib/fakeredis/sort_method.rb b/lib/fakeredis/sort_method.rb
new file mode 100644
index 0000000..4b96ac7
--- /dev/null
+++ b/lib/fakeredis/sort_method.rb
@@ -0,0 +1,116 @@
+# Codes are mostly referenced from MockRedis' implementation.
+module FakeRedis
+  module SortMethod
+    def sort(key, *redis_options_array)
+      return [] unless key
+
+      unless %w(list set zset).include? type(key)
+        warn "Operation against a key holding the wrong kind of value: Expected list, set or zset at #{key}."
+        raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
+      end
+
+      # redis_options is an array of format [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]
+      # Lets nibble it back into a hash
+      options = extract_options_from(redis_options_array)
+
+      # And now to actually do the work of this method
+
+      projected = project(data[key], options[:by], options[:get])
+      sorted    = sort_by(projected, options[:order])
+      sliced    = slice(sorted, options[:limit])
+      # We have to flatten it down as redis-rb adds back the array to the return value
+      result = sliced.flatten(1)
+
+      options[:store] ? rpush(options[:store], sliced) : sliced.flatten(1)
+    end
+
+    private
+
+    ASCENDING_SORT  = Proc.new { |a, b| a.first <=> b.first }
+    DESCENDING_SORT = Proc.new { |a, b| b.first <=> a.first }
+
+    def extract_options_from(options_array)
+      # Defaults
+      options = {
+        :limit => [],
+        :order => "ASC",
+        :get => []
+      }
+
+      if options_array.first == "BY"
+        options_array.shift
+        options[:by] = options_array.shift
+      end
+
+      if options_array.first == "LIMIT"
+        options_array.shift
+        options[:limit] = [options_array.shift, options_array.shift]
+      end
+
+      while options_array.first == "GET"
+        options_array.shift
+        options[:get] << options_array.shift
+      end
+
+      if %w(ASC DESC ALPHA).include?(options_array.first)
+        options[:order] = options_array.shift
+        options[:order] = "ASC" if options[:order] == "ALPHA"
+      end
+
+      if options_array.first == "STORE"
+        options_array.shift
+        options[:store] = options_array.shift
+      end
+
+      options
+    end
+
+    def project(enumerable, by, get_patterns)
+      enumerable.map do |*elements|
+        element = elements.flatten.first
+        weight  = by ? lookup_from_pattern(by, element) : element
+        value   = element
+
+        if get_patterns.length > 0
+          value = get_patterns.map do |pattern|
+            pattern == "#" ? element : lookup_from_pattern(pattern, element)
+          end
+          value = value.first if value.length == 1
+        end
+
+        [weight, value]
+      end
+    end
+
+    def sort_by(projected, direction)
+      sorter =
+        case direction.upcase
+          when "DESC"
+            DESCENDING_SORT
+          when "ASC", "ALPHA"
+            ASCENDING_SORT
+          else
+            raise "Invalid direction '#{direction}'"
+        end
+
+      projected.sort(&sorter).map(&:last)
+    end
+
+    def slice(sorted, limit)
+      skip = limit.first || 0
+      take = limit.last || sorted.length
+
+      sorted[skip...(skip + take)] || sorted
+    end
+
+    def lookup_from_pattern(pattern, element)
+      key = pattern.sub('*', element)
+
+      if (hash_parts = key.split('->')).length > 1
+        hget hash_parts.first, hash_parts.last
+      else
+        get key
+      end
+    end
+  end
+end
diff --git a/lib/fakeredis/sorted_set_argument_handler.rb b/lib/fakeredis/sorted_set_argument_handler.rb
new file mode 100644
index 0000000..086fff2
--- /dev/null
+++ b/lib/fakeredis/sorted_set_argument_handler.rb
@@ -0,0 +1,74 @@
+module FakeRedis
+  # Takes in the variable length array of arguments for a zinterstore/zunionstore method
+  # and parses them into a few attributes for the method to access.
+  #
+  # Handles throwing errors for various scenarios (matches redis):
+  #   * Custom weights specified, but not enough or too many given
+  #   * Invalid aggregate value given
+  #   * Multiple aggregate values given
+  class SortedSetArgumentHandler
+    # [Symbol] The aggregate method to use for the output values. One of %w(sum min max) expected
+    attr_reader :aggregate
+    # [Integer] Number of keys in the argument list
+    attr_accessor :number_of_keys
+    # [Array] The actual keys in the argument list
+    attr_accessor :keys
+    # [Array] integers for weighting the values of each key - one number per key expected
+    attr_accessor :weights
+
+    # Used internally
+    attr_accessor :type
+
+    # Expects all the argments for the method to be passed as an array
+    def initialize args
+      # Pull out known lengths of data
+      self.number_of_keys = args.shift
+      self.keys = args.shift(number_of_keys)
+      # Handle the variable lengths of data (WEIGHTS/AGGREGATE)
+      args.inject(self) {|handler, item| handler.handle(item) }
+
+      # Defaults for unspecified things
+      self.weights ||= Array.new(number_of_keys) { 1 }
+      self.aggregate ||= :sum
+
+      # Validate values
+      raise(Redis::CommandError, "ERR syntax error") unless weights.size == number_of_keys
+      raise(Redis::CommandError, "ERR syntax error") unless [:min, :max, :sum].include?(aggregate)
+    end
+
+    # Only allows assigning a value *once* - raises Redis::CommandError if a second is given
+    def aggregate=(str)
+      raise(Redis::CommandError, "ERR syntax error") if (defined?(@aggregate) && @aggregate)
+      @aggregate = str.to_s.downcase.to_sym
+    end
+
+    # Decides how to handle an item, depending on where we are in the arguments
+    def handle(item)
+      case item
+      when "WEIGHTS"
+        self.type = :weights
+        self.weights = []
+      when "AGGREGATE"
+        self.type = :aggregate
+      when nil
+        # This should never be called, raise a syntax error if we manage to hit it
+        raise(Redis::CommandError, "ERR syntax error")
+      else
+        send "handle_#{type}", item
+      end
+      self
+    end
+
+    def handle_weights(item)
+      self.weights << item
+    end
+
+    def handle_aggregate(item)
+      self.aggregate = item
+    end
+
+    def inject_block
+      lambda { |handler, item| handler.handle(item) }
+    end
+  end
+end
diff --git a/lib/fakeredis/sorted_set_store.rb b/lib/fakeredis/sorted_set_store.rb
new file mode 100644
index 0000000..8ddf703
--- /dev/null
+++ b/lib/fakeredis/sorted_set_store.rb
@@ -0,0 +1,80 @@
+module FakeRedis
+  class SortedSetStore
+    attr_accessor :data, :weights, :aggregate, :keys
+
+    def initialize params, data
+      self.data = data
+      self.weights = params.weights
+      self.aggregate = params.aggregate
+      self.keys = params.keys
+    end
+
+    def hashes
+      @hashes ||= keys.map do |src|
+        case data[src]
+        when ::Set
+          # Every value has a score of 1
+          Hash[data[src].map {|k,v| [k, 1]}]
+        when Hash
+          data[src]
+        else
+          {}
+        end
+      end
+    end
+
+    # Apply the weightings to the hashes
+    def computed_values
+      unless defined?(@computed_values) && @computed_values
+        # Do nothing if all weights are 1, as n * 1 is n
+        @computed_values = hashes if weights.all? {|weight| weight == 1 }
+        # Otherwise, multiply the values in each hash by that hash's weighting
+        @computed_values ||= hashes.each_with_index.map do |hash, index|
+          weight = weights[index]
+          Hash[hash.map {|k, v| [k, (v * weight)]}]
+        end
+      end
+      @computed_values
+    end
+
+    def aggregate_sum out
+      selected_keys.each do |key|
+        out[key] = computed_values.inject(0) do |n, hash|
+          n + (hash[key] || 0)
+        end
+      end
+    end
+
+    def aggregate_min out
+      selected_keys.each do |key|
+        out[key] = computed_values.map {|h| h[key] }.compact.min
+      end
+    end
+
+    def aggregate_max out
+      selected_keys.each do |key|
+        out[key] = computed_values.map {|h| h[key] }.compact.max
+      end
+    end
+
+    def selected_keys
+      raise NotImplemented, "subclass needs to implement #selected_keys"
+    end
+
+    def call
+      ZSet.new.tap {|out| send("aggregate_#{aggregate}", out) }
+    end
+  end
+
+  class SortedSetIntersectStore < SortedSetStore
+    def selected_keys
+      @values ||= hashes.inject([]) { |r, h| r.empty? ? h.keys : (r & h.keys) }
+    end
+  end
+
+  class SortedSetUnionStore < SortedSetStore
+    def selected_keys
+      @values ||= hashes.map(&:keys).flatten.uniq
+    end
+  end
+end
diff --git a/lib/fakeredis/transaction_commands.rb b/lib/fakeredis/transaction_commands.rb
new file mode 100644
index 0000000..65766ec
--- /dev/null
+++ b/lib/fakeredis/transaction_commands.rb
@@ -0,0 +1,83 @@
+module FakeRedis
+  TRANSACTION_COMMANDS = [:discard, :exec, :multi, :watch, :unwatch]
+
+  module TransactionCommands
+    def self.included(klass)
+      klass.class_eval do
+        def self.queued_commands
+          @queued_commands ||= Hash.new {|h,k| h[k] = [] }
+        end
+
+        def self.in_multi
+          @in_multi ||= Hash.new{|h,k| h[k] = false}
+        end
+
+        def queued_commands
+          self.class.queued_commands[database_instance_key]
+        end
+
+        def queued_commands=(cmds)
+          self.class.queued_commands[database_instance_key] = cmds
+        end
+
+        def in_multi
+          self.class.in_multi[database_instance_key]
+        end
+
+        def in_multi=(multi_state)
+          self.class.in_multi[database_instance_key] = multi_state
+        end
+      end
+    end
+
+    def discard
+      unless in_multi
+        raise Redis::CommandError, "ERR DISCARD without MULTI"
+      end
+
+      self.in_multi = false
+      self.queued_commands = []
+
+      'OK'
+    end
+
+    def exec
+      unless in_multi
+        raise Redis::CommandError, "ERR EXEC without MULTI"
+      end
+
+      responses  = queued_commands.map do |cmd|
+        begin
+          send(*cmd)
+        rescue => e
+          e
+        end
+      end
+
+      self.queued_commands = [] # reset queued_commands
+      self.in_multi = false     # reset in_multi state
+
+      responses
+    end
+
+    def multi
+      if in_multi
+        raise Redis::CommandError, "ERR MULTI calls can not be nested"
+      end
+
+      self.in_multi = true
+
+      yield(self) if block_given?
+
+      "OK"
+    end
+
+    def watch(_)
+      "OK"
+    end
+
+    def unwatch
+      "OK"
+    end
+  end
+end
diff --git a/lib/fakeredis/version.rb b/lib/fakeredis/version.rb
new file mode 100644
index 0000000..2c411bd
--- /dev/null
+++ b/lib/fakeredis/version.rb
@@ -0,0 +1,3 @@
+module FakeRedis
+  VERSION = "0.5.0"
+end
diff --git a/lib/fakeredis/zset.rb b/lib/fakeredis/zset.rb
new file mode 100644
index 0000000..5be7566
--- /dev/null
+++ b/lib/fakeredis/zset.rb
@@ -0,0 +1,33 @@
+module FakeRedis
+  class ZSet < Hash
+
+    def []=(key, val)
+      super(key, _floatify(val))
+    end
+
+    # Increments the value of key by val
+    def increment(key, val)
+      self[key] += _floatify(val)
+    end
+
+    def select_by_score min, max
+      min = _floatify(min, true)
+      max = _floatify(max, false)
+      reject {|_,v| v < min || v > max }
+    end
+
+    private
+
+    # Originally lifted from redis-rb
+    def _floatify(str, increment = true)
+      if (( inf = str.to_s.match(/^([+-])?inf/i) ))
+        (inf[1] == "-" ? -1.0 : 1.0) / 0.0
+      elsif (( number = str.to_s.match(/^\((\d+)/i) ))
+        number[1].to_i + (increment ? 1 : -1)
+      else
+        Float str
+      end
+    end
+
+  end
+end
diff --git a/lib/redis/connection/memory.rb b/lib/redis/connection/memory.rb
new file mode 100644
index 0000000..2ea9a27
--- /dev/null
+++ b/lib/redis/connection/memory.rb
@@ -0,0 +1,1034 @@
+require 'set'
+require 'redis/connection/registry'
+require 'redis/connection/command_helper'
+require "fakeredis/command_executor"
+require "fakeredis/expiring_hash"
+require "fakeredis/sort_method"
+require "fakeredis/sorted_set_argument_handler"
+require "fakeredis/sorted_set_store"
+require "fakeredis/transaction_commands"
+require "fakeredis/zset"
+
+class Redis
+  module Connection
+    class Memory
+      include Redis::Connection::CommandHelper
+      include FakeRedis
+      include SortMethod
+      include TransactionCommands
+      include CommandExecutor
+
+      attr_accessor :options
+
+      # Tracks all databases for all instances across the current process.
+      # We have to be able to handle two clients with the same host/port accessing
+      # different databases at once without overwriting each other. So we store our
+      # "data" outside the client instances, in this class level instance method.
+      # Client instances access it with a key made up of their host/port, and then select
+      # which DB out of the array of them they want. Allows the access we need.
+      def self.databases
+        @databases ||= Hash.new {|h,k| h[k] = [] }
+      end
+
+      # Used for resetting everything in specs
+      def self.reset_all_databases
+        @databases = nil
+      end
+
+      def self.connect(options = {})
+        new(options)
+      end
+
+      def initialize(options = {})
+        self.options = options
+      end
+
+      def database_id
+        @database_id ||= 0
+      end
+      attr_writer :database_id
+
+      def database_instance_key
+        [options[:host], options[:port]].hash
+      end
+
+      def databases
+        self.class.databases[database_instance_key]
+      end
+
+      def find_database id=database_id
+        databases[id] ||= ExpiringHash.new
+      end
+
+      def data
+        find_database
+      end
+
+      def replies
+        @replies ||= []
+      end
+      attr_writer :replies
+
+      def connected?
+        true
+      end
+
+      def connect_unix(path, timeout)
+      end
+
+      def disconnect
+      end
+
+      def timeout=(usecs)
+      end
+
+      def read
+        replies.shift
+      end
+
+      # NOT IMPLEMENTED:
+      # * blpop
+      # * brpop
+      # * brpoplpush
+      # * subscribe
+      # * psubscribe
+      # * publish
+
+      def flushdb
+        databases.delete_at(database_id)
+        "OK"
+      end
+
+      def flushall
+        self.class.databases[database_instance_key] = []
+        "OK"
+      end
+
+      def auth(password)
+        "OK"
+      end
+
+      def select(index)
+        data_type_check(index, Integer)
+        self.database_id = index
+        "OK"
+      end
+
+      def info
+        {
+          "redis_version" => "2.6.16",
+          "connected_clients" => "1",
+          "connected_slaves" => "0",
+          "used_memory" => "3187",
+          "changes_since_last_save" => "0",
+          "last_save_time" => "1237655729",
+          "total_connections_received" => "1",
+          "total_commands_processed" => "1",
+          "uptime_in_seconds" => "36000",
+          "uptime_in_days" => 0
+        }
+      end
+
+      def monitor; end
+
+      def save; end
+
+      def bgsave ; end
+
+      def bgrewriteaof ; end
+
+      def move key, destination_id
+        raise Redis::CommandError, "ERR source and destination objects are the same" if destination_id == database_id
+        destination = find_database(destination_id)
+        return false unless data.has_key?(key)
+        return false if destination.has_key?(key)
+        destination[key] = data.delete(key)
+        true
+      end
+
+      def get(key)
+        data_type_check(key, String)
+        data[key]
+      end
+
+      def getbit(key, offset)
+        return unless data[key]
+        data[key].unpack('B*')[0].split("")[offset].to_i
+      end
+
+      def bitcount(key, start_index = 0, end_index = -1)
+        return 0 unless data[key]
+        data[key][start_index..end_index].unpack('B*')[0].count("1")
+      end
+
+      def getrange(key, start, ending)
+        return unless data[key]
+        data[key][start..ending]
+      end
+      alias :substr :getrange
+
+      def getset(key, value)
+        data_type_check(key, String)
+        data[key].tap do
+          set(key, value)
+        end
+      end
+
+      def mget(*keys)
+        raise_argument_error('mget') if keys.empty?
+        # We work with either an array, or list of arguments
+        keys = keys.first if keys.size == 1
+        data.values_at(*keys)
+      end
+
+      def append(key, value)
+        data[key] = (data[key] || "")
+        data[key] = data[key] + value.to_s
+      end
+
+      def strlen(key)
+        return unless data[key]
+        data[key].size
+      end
+
+      def hgetall(key)
+        data_type_check(key, Hash)
+        data[key].to_a.flatten || {}
+      end
+
+      def hget(key, field)
+        data_type_check(key, Hash)
+        data[key] && data[key][field.to_s]
+      end
+
+      def hdel(key, field)
+        field = field.to_s
+        data_type_check(key, Hash)
+        deleted = data[key] && data[key].delete(field)
+        remove_key_for_empty_collection(key)
+        deleted ? 1 : 0
+      end
+
+      def hkeys(key)
+        data_type_check(key, Hash)
+        return [] if data[key].nil?
+        data[key].keys
+      end
+
+      def keys(pattern = "*")
+        data.keys.select { |key| File.fnmatch(pattern, key) }
+      end
+
+      def randomkey
+        data.keys[rand(dbsize)]
+      end
+
+      def echo(string)
+        string
+      end
+
+      def ping
+        "PONG"
+      end
+
+      def lastsave
+        Time.now.to_i
+      end
+
+      def time
+        microseconds = (Time.now.to_f * 1000000).to_i
+        [ microseconds / 1000000, microseconds % 1000000 ]
+      end
+
+      def dbsize
+        data.keys.count
+      end
+
+      def exists(key)
+        data.key?(key)
+      end
+
+      def llen(key)
+        data_type_check(key, Array)
+        return 0 unless data[key]
+        data[key].size
+      end
+
+      def lrange(key, startidx, endidx)
+        data_type_check(key, Array)
+        (data[key] && data[key][startidx..endidx]) || []
+      end
+
+      def ltrim(key, start, stop)
+        data_type_check(key, Array)
+        return unless data[key]
+
+        # Example: we have a list of 3 elements and
+        # we give it a ltrim list, -5, -1. This means
+        # it should trim to a max of 5. Since 3 < 5
+        # we should not touch the list. This is consistent
+        # with behavior of real Redis's ltrim with a negative
+        # start argument.
+        unless start < 0 && data[key].count < start.abs
+          data[key] = data[key][start..stop]
+        end
+
+        "OK"
+      end
+
+      def lindex(key, index)
+        data_type_check(key, Array)
+        data[key] && data[key][index]
+      end
+
+      def linsert(key, where, pivot, value)
+        data_type_check(key, Array)
+        return unless data[key]
+        index = data[key].index(pivot)
+        case where
+          when :before then data[key].insert(index, value)
+          when :after  then data[key].insert(index + 1, value)
+          else raise_syntax_error
+        end
+      end
+
+      def lset(key, index, value)
+        data_type_check(key, Array)
+        return unless data[key]
+        raise Redis::CommandError, "ERR index out of range" if index >= data[key].size
+        data[key][index] = value
+      end
+
+      def lrem(key, count, value)
+        data_type_check(key, Array)
+        return unless data[key]
+        old_size = data[key].size
+        diff =
+          if count == 0
+            data[key].delete(value)
+            old_size - data[key].size
+          else
+            array = count > 0 ? data[key].dup : data[key].reverse
+            count.abs.times{ array.delete_at(array.index(value) || array.length) }
+            data[key] = count > 0 ? array.dup : array.reverse
+            old_size - data[key].size
+          end
+        remove_key_for_empty_collection(key)
+        diff
+      end
+
+      def rpush(key, value)
+        data_type_check(key, Array)
+        data[key] ||= []
+        [value].flatten.each do |val|
+          data[key].push(val.to_s)
+        end
+        data[key].size
+      end
+
+      def rpushx(key, value)
+        data_type_check(key, Array)
+        return unless data[key]
+        rpush(key, value)
+      end
+
+      def lpush(key, value)
+        data_type_check(key, Array)
+        data[key] ||= []
+        [value].flatten.each do |val|
+          data[key].unshift(val.to_s)
+        end
+        data[key].size
+      end
+
+      def lpushx(key, value)
+        data_type_check(key, Array)
+        return unless data[key]
+        lpush(key, value)
+      end
+
+      def rpop(key)
+        data_type_check(key, Array)
+        return unless data[key]
+        data[key].pop
+      end
+
+      def rpoplpush(key1, key2)
+        data_type_check(key1, Array)
+        rpop(key1).tap do |elem|
+          lpush(key2, elem) unless elem.nil?
+        end
+      end
+
+      def lpop(key)
+        data_type_check(key, Array)
+        return unless data[key]
+        data[key].shift
+      end
+
+      def smembers(key)
+        data_type_check(key, ::Set)
+        return [] unless data[key]
+        data[key].to_a.reverse
+      end
+
+      def sismember(key, value)
+        data_type_check(key, ::Set)
+        return false unless data[key]
+        data[key].include?(value.to_s)
+      end
+
+      def sadd(key, value)
+        data_type_check(key, ::Set)
+        value = Array(value)
+        raise_argument_error('sadd') if value.empty?
+
+        result = if data[key]
+          old_set = data[key].dup
+          data[key].merge(value.map(&:to_s))
+          (data[key] - old_set).size
+        else
+          data[key] = ::Set.new(value.map(&:to_s))
+          data[key].size
+        end
+
+        # 0 = false, 1 = true, 2+ untouched
+        return result == 1 if result < 2
+        result
+      end
+
+      def srem(key, value)
+        data_type_check(key, ::Set)
+        return false unless data[key]
+
+        if value.is_a?(Array)
+          old_size = data[key].size
+          values = value.map(&:to_s)
+          values.each { |value| data[key].delete(value) }
+          deleted = old_size - data[key].size
+        else
+          deleted = !!data[key].delete?(value.to_s)
+        end
+
+        remove_key_for_empty_collection(key)
+        deleted
+      end
+
+      def smove(source, destination, value)
+        data_type_check(destination, ::Set)
+        result = self.srem(source, value)
+        self.sadd(destination, value) if result
+        result
+      end
+
+      def spop(key)
+        data_type_check(key, ::Set)
+        elem = srandmember(key)
+        srem(key, elem)
+        elem
+      end
+
+      def scard(key)
+        data_type_check(key, ::Set)
+        return 0 unless data[key]
+        data[key].size
+      end
+
+      def sinter(*keys)
+        raise_argument_error('sinter') if keys.empty?
+
+        keys.each { |k| data_type_check(k, ::Set) }
+        return ::Set.new if keys.any? { |k| data[k].nil? }
+        keys = keys.map { |k| data[k] || ::Set.new }
+        keys.inject do |set, key|
+          set & key
+        end.to_a
+      end
+
+      def sinterstore(destination, *keys)
+        data_type_check(destination, ::Set)
+        result = sinter(*keys)
+        data[destination] = ::Set.new(result)
+      end
+
+      def sunion(*keys)
+        keys.each { |k| data_type_check(k, ::Set) }
+        keys = keys.map { |k| data[k] || ::Set.new }
+        keys.inject(::Set.new) do |set, key|
+          set | key
+        end.to_a
+      end
+
+      def sunionstore(destination, *keys)
+        data_type_check(destination, ::Set)
+        result = sunion(*keys)
+        data[destination] = ::Set.new(result)
+      end
+
+      def sdiff(key1, *keys)
+        [key1, *keys].each { |k| data_type_check(k, ::Set) }
+        keys = keys.map { |k| data[k] || ::Set.new }
+        keys.inject(data[key1] || Set.new) do |memo, set|
+          memo - set
+        end.to_a
+      end
+
+      def sdiffstore(destination, key1, *keys)
+        data_type_check(destination, ::Set)
+        result = sdiff(key1, *keys)
+        data[destination] = ::Set.new(result)
+      end
+
+      def srandmember(key, number=nil)
+        number.nil? ? srandmember_single(key) : srandmember_multiple(key, number)
+      end
+
+      def del(*keys)
+        keys = keys.flatten(1)
+        raise_argument_error('del') if keys.empty?
+
+        old_count = data.keys.size
+        keys.each do |key|
+          data.delete(key)
+        end
+        old_count - data.keys.size
+      end
+
+      def setnx(key, value)
+        if exists(key)
+          false
+        else
+          set(key, value)
+          true
+        end
+      end
+
+      def rename(key, new_key)
+        return unless data[key]
+        data[new_key] = data[key]
+        data.expires[new_key] = data.expires[key] if data.expires.include?(key)
+        data.delete(key)
+      end
+
+      def renamenx(key, new_key)
+        if exists(new_key)
+          false
+        else
+          rename(key, new_key)
+          true
+        end
+      end
+
+      def expire(key, ttl)
+        return 0 unless data[key]
+        data.expires[key] = Time.now + ttl
+        1
+      end
+
+      def ttl(key)
+        if data.expires.include?(key) && (ttl = data.expires[key].to_i - Time.now.to_i) > 0
+          ttl
+        else
+          exists(key) ? -1 : -2
+        end
+      end
+
+      def expireat(key, timestamp)
+        data.expires[key] = Time.at(timestamp)
+        true
+      end
+
+      def persist(key)
+        !!data.expires.delete(key)
+      end
+
+      def hset(key, field, value)
+        data_type_check(key, Hash)
+        field = field.to_s
+        if data[key]
+          result = !data[key].include?(field)
+          data[key][field] = value.to_s
+          result
+        else
+          data[key] = { field => value.to_s }
+          true
+        end
+      end
+
+      def hsetnx(key, field, value)
+        data_type_check(key, Hash)
+        field = field.to_s
+        return false if data[key] && data[key][field]
+        hset(key, field, value)
+      end
+
+      def hmset(key, *fields)
+        # mapped_hmset gives us [[:k1, "v1", :k2, "v2"]] for `fields`. Fix that.
+        fields = fields[0] if mapped_param?(fields)
+        raise_argument_error('hmset') if fields.empty?
+
+        is_list_of_arrays = fields.all?{|field| field.instance_of?(Array)}
+
+        raise_argument_error('hmset') if fields.size.odd? and !is_list_of_arrays
+        raise_argument_error('hmset') if is_list_of_arrays and !fields.all?{|field| field.length == 2}
+
+        data_type_check(key, Hash)
+        data[key] ||= {}
+
+        if is_list_of_arrays
+          fields.each do |pair|
+            data[key][pair[0].to_s] = pair[1].to_s
+          end
+        else
+          fields.each_slice(2) do |field|
+            data[key][field[0].to_s] = field[1].to_s
+          end
+        end
+      end
+
+      def hmget(key, *fields)
+        raise_argument_error('hmget')  if fields.empty?
+
+        data_type_check(key, Hash)
+        fields.flatten.map do |field|
+          field = field.to_s
+          if data[key]
+            data[key][field]
+          else
+            nil
+          end
+        end
+      end
+
+      def hlen(key)
+        data_type_check(key, Hash)
+        return 0 unless data[key]
+        data[key].size
+      end
+
+      def hvals(key)
+        data_type_check(key, Hash)
+        return [] unless data[key]
+        data[key].values
+      end
+
+      def hincrby(key, field, increment)
+        data_type_check(key, Hash)
+        field = field.to_s
+        if data[key]
+          data[key][field] = (data[key][field].to_i + increment.to_i).to_s
+        else
+          data[key] = { field => increment.to_s }
+        end
+        data[key][field].to_i
+      end
+
+      def hincrbyfloat(key, field, increment)
+        data_type_check(key, Hash)
+        field = field.to_s
+        if data[key]
+          data[key][field] = (data[key][field].to_f + increment.to_f).to_s
+        else
+          data[key] = { field => increment.to_s }
+        end
+        data[key][field]
+      end
+
+      def hexists(key, field)
+        data_type_check(key, Hash)
+        return false unless data[key]
+        data[key].key?(field.to_s)
+      end
+
+      def sync ; end
+
+      def [](key)
+        get(key)
+      end
+
+      def []=(key, value)
+        set(key, value)
+      end
+
+      def set(key, value, *array_options)
+        option_nx = array_options.delete("NX")
+        option_xx = array_options.delete("XX")
+
+        return false if option_nx && option_xx
+
+        return false if option_nx && exists(key)
+        return false if option_xx && !exists(key)
+
+        data[key] = value.to_s
+
+        options = Hash[array_options.each_slice(2).to_a]
+        ttl_in_seconds = options["EX"] if options["EX"]
+        ttl_in_seconds = options["PX"] / 1000.0 if options["PX"]
+
+        expire(key, ttl_in_seconds) if ttl_in_seconds
+
+        "OK"
+      end
+
+      def setbit(key, offset, bit)
+        old_val = data[key] ? data[key].unpack('B*')[0].split("") : []
+        size_increment = [((offset/8)+1)*8-old_val.length, 0].max
+        old_val += Array.new(size_increment).map{"0"}
+        original_val = old_val[offset].to_i
+        old_val[offset] = bit.to_s
+        new_val = ""
+        old_val.each_slice(8){|b| new_val = new_val + b.join("").to_i(2).chr }
+        data[key] = new_val
+        original_val
+      end
+
+      def setex(key, seconds, value)
+        data[key] = value.to_s
+        expire(key, seconds)
+        "OK"
+      end
+
+      def setrange(key, offset, value)
+        return unless data[key]
+        s = data[key][offset,value.size]
+        data[key][s] = value
+      end
+
+      def mset(*pairs)
+        # Handle pairs for mapped_mset command
+        pairs = pairs[0] if mapped_param?(pairs)
+        raise_argument_error('mset') if pairs.empty? || pairs.size == 1
+        # We have to reply with a different error message here to be consistent with redis-rb 3.0.6 / redis-server 2.8.1
+        raise_argument_error("mset", "mset_odd") if pairs.size.odd?
+
+        pairs.each_slice(2) do |pair|
+          data[pair[0].to_s] = pair[1].to_s
+        end
+        "OK"
+      end
+
+      def msetnx(*pairs)
+        # Handle pairs for mapped_msetnx command
+        pairs = pairs[0] if mapped_param?(pairs)
+        keys = []
+        pairs.each_with_index{|item, index| keys << item.to_s if index % 2 == 0}
+        return false if keys.any?{|key| data.key?(key) }
+        mset(*pairs)
+        true
+      end
+
+      def incr(key)
+        data.merge!({ key => (data[key].to_i + 1).to_s || "1"})
+        data[key].to_i
+      end
+
+      def incrby(key, by)
+        data.merge!({ key => (data[key].to_i + by.to_i).to_s || by })
+        data[key].to_i
+      end
+
+      def decr(key)
+        data.merge!({ key => (data[key].to_i - 1).to_s || "-1"})
+        data[key].to_i
+      end
+
+      def decrby(key, by)
+        data.merge!({ key => ((data[key].to_i - by.to_i) || (by.to_i * -1)).to_s })
+        data[key].to_i
+      end
+
+      def type(key)
+        case data[key]
+          when nil then "none"
+          when String then "string"
+          when ZSet then "zset"
+          when Hash then "hash"
+          when Array then "list"
+          when ::Set then "set"
+        end
+      end
+
+      def quit ; end
+
+      def shutdown; end
+
+      def slaveof(host, port) ; end
+
+      def scan(start_cursor, *args)
+        match = "*"
+        count = 10
+
+        if args.size.odd?
+          raise_argument_error('scan')
+        end
+
+        if idx = args.index("MATCH")
+          match = args[idx + 1]
+        end
+
+        if idx = args.index("COUNT")
+          count = args[idx + 1]
+        end
+
+        start_cursor = start_cursor.to_i
+        data_type_check(start_cursor, Fixnum)
+
+        cursor = start_cursor
+        next_keys = []
+
+        if start_cursor + count >= data.length
+          next_keys = keys(match)[start_cursor..-1]
+          cursor = 0
+        else
+          cursor = start_cursor + 10
+          next_keys = keys(match)[start_cursor..cursor]
+        end
+
+        return "#{cursor}", next_keys
+      end
+
+      def zadd(key, *args)
+        if !args.first.is_a?(Array)
+          if args.size < 2
+            raise_argument_error('zadd')
+          elsif args.size.odd?
+            raise_syntax_error
+          end
+        else
+          unless args.all? {|pair| pair.size == 2 }
+            raise_syntax_error
+          end
+        end
+
+        data_type_check(key, ZSet)
+        data[key] ||= ZSet.new
+
+        if args.size == 2 && !(Array === args.first)
+          score, value = args
+          exists = !data[key].key?(value.to_s)
+          data[key][value.to_s] = score
+        else
+          # Turn [1, 2, 3, 4] into [[1, 2], [3, 4]] unless it is already
+          args = args.each_slice(2).to_a unless args.first.is_a?(Array)
+          exists = args.map(&:last).map { |el| data[key].key?(el.to_s) }.count(false)
+          args.each { |s, v| data[key][v.to_s] = s }
+        end
+
+        exists
+      end
+
+      def zrem(key, value)
+        data_type_check(key, ZSet)
+        values = Array(value)
+        return 0 unless data[key]
+
+        response = values.map do |v|
+          data[key].delete(v.to_s) if data[key].has_key?(v.to_s)
+        end.compact.size
+
+        remove_key_for_empty_collection(key)
+        response
+      end
+
+      def zcard(key)
+        data_type_check(key, ZSet)
+        data[key] ? data[key].size : 0
+      end
+
+      def zscore(key, value)
+        data_type_check(key, ZSet)
+        value = data[key] && data[key][value.to_s]
+        value && value.to_s
+      end
+
+      def zcount(key, min, max)
+        data_type_check(key, ZSet)
+        return 0 unless data[key]
+        data[key].select_by_score(min, max).size
+      end
+
+      def zincrby(key, num, value)
+        data_type_check(key, ZSet)
+        data[key] ||= ZSet.new
+        data[key][value.to_s] ||= 0
+        data[key].increment(value.to_s, num)
+        data[key][value.to_s].to_s
+      end
+
+      def zrank(key, value)
+        data_type_check(key, ZSet)
+        z = data[key]
+        return unless z
+        z.keys.sort_by {|k| z[k] }.index(value.to_s)
+      end
+
+      def zrevrank(key, value)
+        data_type_check(key, ZSet)
+        z = data[key]
+        return unless z
+        z.keys.sort_by {|k| -z[k] }.index(value.to_s)
+      end
+
+      def zrange(key, start, stop, with_scores = nil)
+        data_type_check(key, ZSet)
+        return [] unless data[key]
+
+        # Sort by score, or if scores are equal, key alphanum
+        results = data[key].sort do |(k1, v1), (k2, v2)|
+          if v1 == v2
+            k1 <=> k2
+          else
+            v1 <=> v2
+          end
+        end
+        # Select just the keys unless we want scores
+        results = results.map(&:first) unless with_scores
+        results[start..stop].flatten.map(&:to_s)
+      end
+
+      def zrevrange(key, start, stop, with_scores = nil)
+        data_type_check(key, ZSet)
+        return [] unless data[key]
+
+        if with_scores
+          data[key].sort_by {|_,v| -v }
+        else
+          data[key].keys.sort_by {|k| -data[key][k] }
+        end[start..stop].flatten.map(&:to_s)
+      end
+
+      def zrangebyscore(key, min, max, *opts)
+        data_type_check(key, ZSet)
+        return [] unless data[key]
+
+        range = data[key].select_by_score(min, max)
+        vals = if opts.include?('WITHSCORES')
+          range.sort_by {|_,v| v }
+        else
+          range.keys.sort_by {|k| range[k] }
+        end
+
+        limit = get_limit(opts, vals)
+        vals = vals[*limit] if limit
+
+        vals.flatten.map(&:to_s)
+      end
+
+      def zrevrangebyscore(key, max, min, *opts)
+        opts = opts.flatten
+        data_type_check(key, ZSet)
+        return [] unless data[key]
+
+        range = data[key].select_by_score(min, max)
+        vals = if opts.include?('WITHSCORES')
+          range.sort_by {|_,v| -v }
+        else
+          range.keys.sort_by {|k| -range[k] }
+        end
+
+        limit = get_limit(opts, vals)
+        vals = vals[*limit] if limit
+
+        vals.flatten.map(&:to_s)
+      end
+
+      def zremrangebyscore(key, min, max)
+        data_type_check(key, ZSet)
+        return 0 unless data[key]
+
+        range = data[key].select_by_score(min, max)
+        range.each {|k,_| data[key].delete(k) }
+        range.size
+      end
+
+      def zremrangebyrank(key, start, stop)
+        data_type_check(key, ZSet)
+        return 0 unless data[key]
+
+        sorted_elements = data[key].sort_by { |k, v| v }
+        start = sorted_elements.length if start > sorted_elements.length
+        elements_to_delete = sorted_elements[start..stop]
+        elements_to_delete.each { |elem, rank| data[key].delete(elem) }
+        elements_to_delete.size
+      end
+
+      def zinterstore(out, *args)
+        data_type_check(out, ZSet)
+        args_handler = SortedSetArgumentHandler.new(args)
+        data[out] = SortedSetIntersectStore.new(args_handler, data).call
+        data[out].size
+      end
+
+      def zunionstore(out, *args)
+        data_type_check(out, ZSet)
+        args_handler = SortedSetArgumentHandler.new(args)
+        data[out] = SortedSetUnionStore.new(args_handler, data).call
+        data[out].size
+      end
+
+      private
+        def raise_argument_error(command, match_string=command)
+          error_message = if %w(hmset mset_odd).include?(match_string.downcase)
+            "ERR wrong number of arguments for #{command.upcase}"
+          else
+            "ERR wrong number of arguments for '#{command}' command"
+          end
+
+          raise Redis::CommandError, error_message
+        end
+
+        def raise_syntax_error
+          raise Redis::CommandError, "ERR syntax error"
+        end
+
+        def remove_key_for_empty_collection(key)
+          del(key) if data[key] && data[key].empty?
+        end
+
+        def data_type_check(key, klass)
+          if data[key] && !data[key].is_a?(klass)
+            warn "Operation against a key holding the wrong kind of value: Expected #{klass} at #{key}."
+            raise Redis::CommandError.new("WRONGTYPE Operation against a key holding the wrong kind of value")
+          end
+        end
+
+        def get_limit(opts, vals)
+          index = opts.index('LIMIT')
+
+          if index
+            offset = opts[index + 1]
+
+            count = opts[index + 2]
+            count = vals.size if count < 0
+
+            [offset, count]
+          end
+        end
+
+        def mapped_param? param
+          param.size == 1 && param[0].is_a?(Array)
+        end
+
+        def srandmember_single(key)
+          data_type_check(key, ::Set)
+          return nil unless data[key]
+          data[key].to_a[rand(data[key].size)]
+        end
+
+        def srandmember_multiple(key, number)
+          return [] unless data[key]
+          if number >= 0
+            # replace with `data[key].to_a.sample(number)` when 1.8.7 is deprecated
+            (1..number).inject([]) do |selected, _|
+              available_elements = data[key].to_a - selected
+              selected << available_elements[rand(available_elements.size)]
+            end.compact
+          else
+            (1..-number).map { data[key].to_a[rand(data[key].size)] }.flatten
+          end
+        end
+    end
+  end
+end
+
+Redis::Connection.drivers << Redis::Connection::Memory
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..40d7dde
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,124 @@
+--- !ruby/object:Gem::Specification
+name: fakeredis
+version: !ruby/object:Gem::Version
+  version: 0.5.0
+platform: ruby
+authors:
+- Guillermo Iguaran
+autorequire: 
+bindir: bin
+cert_chain: []
+date: 2014-07-01 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+  name: redis
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - "~>"
+      - !ruby/object:Gem::Version
+        version: '3.0'
+  type: :runtime
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - "~>"
+      - !ruby/object:Gem::Version
+        version: '3.0'
+- !ruby/object:Gem::Dependency
+  name: rspec
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - "~>"
+      - !ruby/object:Gem::Version
+        version: '3.0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - "~>"
+      - !ruby/object:Gem::Version
+        version: '3.0'
+description: Fake (In-memory) driver for redis-rb. Useful for testing environment
+  and machines without Redis.
+email:
+- guilleiguaran at gmail.com
+executables: []
+extensions: []
+extra_rdoc_files: []
+files:
+- ".gitignore"
+- ".travis.yml"
+- Gemfile
+- LICENSE
+- README.md
+- Rakefile
+- fakeredis.gemspec
+- lib/fake_redis.rb
+- lib/fakeredis.rb
+- lib/fakeredis/command_executor.rb
+- lib/fakeredis/expiring_hash.rb
+- lib/fakeredis/rspec.rb
+- lib/fakeredis/sort_method.rb
+- lib/fakeredis/sorted_set_argument_handler.rb
+- lib/fakeredis/sorted_set_store.rb
+- lib/fakeredis/transaction_commands.rb
+- lib/fakeredis/version.rb
+- lib/fakeredis/zset.rb
+- lib/redis/connection/memory.rb
+- spec/compatibility_spec.rb
+- spec/connection_spec.rb
+- spec/hashes_spec.rb
+- spec/keys_spec.rb
+- spec/lists_spec.rb
+- spec/memory_spec.rb
+- spec/server_spec.rb
+- spec/sets_spec.rb
+- spec/sort_method_spec.rb
+- spec/sorted_sets_spec.rb
+- spec/spec_helper.rb
+- spec/spec_helper_live_redis.rb
+- spec/strings_spec.rb
+- spec/support/shared_examples/sortable.rb
+- spec/transactions_spec.rb
+- spec/upcase_method_name_spec.rb
+homepage: https://guilleiguaran.github.com/fakeredis
+licenses:
+- MIT
+metadata: {}
+post_install_message: 
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ">="
+    - !ruby/object:Gem::Version
+      version: '0'
+required_rubygems_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ">="
+    - !ruby/object:Gem::Version
+      version: '0'
+requirements: []
+rubyforge_project: 
+rubygems_version: 2.2.0
+signing_key: 
+specification_version: 4
+summary: Fake (In-memory) driver for redis-rb.
+test_files:
+- spec/compatibility_spec.rb
+- spec/connection_spec.rb
+- spec/hashes_spec.rb
+- spec/keys_spec.rb
+- spec/lists_spec.rb
+- spec/memory_spec.rb
+- spec/server_spec.rb
+- spec/sets_spec.rb
+- spec/sort_method_spec.rb
+- spec/sorted_sets_spec.rb
+- spec/spec_helper.rb
+- spec/spec_helper_live_redis.rb
+- spec/strings_spec.rb
+- spec/support/shared_examples/sortable.rb
+- spec/transactions_spec.rb
+- spec/upcase_method_name_spec.rb
diff --git a/spec/compatibility_spec.rb b/spec/compatibility_spec.rb
new file mode 100644
index 0000000..6871025
--- /dev/null
+++ b/spec/compatibility_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "Compatibility" do
+    it "should be accessible through FakeRedis::Redis" do
+      lambda { FakeRedis::Redis.new }.should_not raise_error
+    end
+  end
+end
diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb
new file mode 100644
index 0000000..a70c6a6
--- /dev/null
+++ b/spec/connection_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "ConnectionMethods" do
+
+    before(:each) do
+      @client = Redis.new
+    end
+
+  if fakeredis?
+    it "should authenticate to the server" do
+      @client.auth("pass").should be == "OK"
+    end
+
+    it "should re-use the same instance with the same host & port" do
+      @client1 = Redis.new(:host => "localhost", :port => 1234)
+      @client2 = Redis.new(:host => "localhost", :port => 1234)
+      @client3 = Redis.new(:host => "localhost", :port => 5678)
+
+      @client1.set("key1", "1")
+      @client2.get("key1").should be == "1"
+      @client3.get("key1").should be_nil
+
+      @client2.set("key2", "2")
+      @client1.get("key2").should be == "2"
+      @client3.get("key2").should be_nil
+
+      @client3.set("key3", "3")
+      @client1.get("key3").should be_nil
+      @client2.get("key3").should be_nil
+
+      @client1.dbsize.should be == 2
+      @client2.dbsize.should be == 2
+      @client3.dbsize.should be == 1
+    end
+
+    it "should connect to a specific database" do
+      @client1 = Redis.new(:host => "localhost", :port => 1234, :db => 0)
+      @client1.set("key1", "1")
+      @client1.select(0)
+      @client1.get("key1").should be == "1"
+
+      @client2 = Redis.new(:host => "localhost", :port => 1234, :db => 1)
+      @client2.set("key1", "1")
+      @client2.select(1)
+      @client2.get("key1").should == "1"
+    end
+
+    it "should not error with shutdown" do
+      lambda { @client.shutdown }.should_not raise_error
+    end
+
+    it "should not error with quit" do
+      lambda { @client.quit }.should_not raise_error
+    end
+  end
+
+    it "should handle multiple clients using the same db instance" do
+      @client1 = Redis.new(:host => "localhost", :port => 6379, :db => 1)
+      @client2 = Redis.new(:host => "localhost", :port => 6379, :db => 2)
+
+      @client1.set("key1", "one")
+      @client1.get("key1").should be == "one"
+
+      @client2.set("key2", "two")
+      @client2.get("key2").should be == "two"
+
+      @client1.get("key1").should be == "one"
+    end
+
+    it "should not error with a disconnected client" do
+      @client1 = Redis.new
+      @client1.client.disconnect
+      @client1.get("key1").should be_nil
+    end
+
+    it "should echo the given string" do
+      @client.echo("something").should == "something"
+    end
+
+    it "should ping the server" do
+      @client.ping.should == "PONG"
+    end
+  end
+end
diff --git a/spec/hashes_spec.rb b/spec/hashes_spec.rb
new file mode 100644
index 0000000..8945dc9
--- /dev/null
+++ b/spec/hashes_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "HashesMethods" do
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should delete a hash field" do
+      @client.hset("key1", "k1", "val1")
+      @client.hset("key1", "k2", "val2")
+      @client.hdel("key1", "k1").should be(1)
+
+      @client.hget("key1", "k1").should be_nil
+      @client.hget("key1", "k2").should be == "val2"
+    end
+
+    it "should remove a hash with no keys left" do
+      @client.hset("key1", "k1", "val1")
+      @client.hset("key1", "k2", "val2")
+      @client.hdel("key1", "k1").should be(1)
+      @client.hdel("key1", "k2").should be(1)
+
+      @client.exists("key1").should be == false
+    end
+
+    it "should convert key to a string for hset" do
+      m = double("key")
+      m.stub(:to_s).and_return("foo")
+
+      @client.hset("key1", m, "bar")
+      @client.hget("key1", "foo").should be == "bar"
+    end
+
+    it "should convert key to a string for hget" do
+      m = double("key")
+      m.stub(:to_s).and_return("foo")
+
+      @client.hset("key1", "foo", "bar")
+      @client.hget("key1", m).should be == "bar"
+    end
+
+    it "should determine if a hash field exists" do
+      @client.hset("key1", "index", "value")
+
+      @client.hexists("key1", "index").should be true
+      @client.hexists("key2", "i2").should be false
+    end
+
+    it "should get the value of a hash field" do
+      @client.hset("key1", "index", "value")
+
+      @client.hget("key1", "index").should be == "value"
+    end
+
+    it "should get all the fields and values in a hash" do
+      @client.hset("key1", "i1", "val1")
+      @client.hset("key1", "i2", "val2")
+
+      @client.hgetall("key1").should be == {"i1" => "val1", "i2" => "val2"}
+    end
+
+    it "should increment the integer value of a hash field by the given number" do
+      @client.hset("key1", "cont1", "5")
+      @client.hincrby("key1", "cont1", "5").should be == 10
+      @client.hget("key1", "cont1").should be == "10"
+    end
+
+    it "should increment non existing hash keys" do
+      @client.hget("key1", "cont2").should be_nil
+      @client.hincrby("key1", "cont2", "5").should be == 5
+    end
+
+    it "should increment the float value of a hash field by the given float" do
+      @client.hset("key1", "cont1", 5.0)
+      @client.hincrbyfloat("key1", "cont1", 4.1).should be == 9.1
+      @client.hget("key1", "cont1").should be == "9.1"
+    end
+
+    it "should increment non existing hash keys" do
+      @client.hget("key1", "cont2").should be_nil
+      @client.hincrbyfloat("key1", "cont2", 5.5).should be == 5.5
+    end
+
+    it "should get all the fields in a hash" do
+      @client.hset("key1", "i1", "val1")
+      @client.hset("key1", "i2", "val2")
+
+      @client.hkeys("key1").should =~ ["i1", "i2"]
+      @client.hkeys("key2").should be == []
+    end
+
+    it "should get the number of fields in a hash" do
+      @client.hset("key1", "i1", "val1")
+      @client.hset("key1", "i2", "val2")
+
+      @client.hlen("key1").should be == 2
+    end
+
+    it "should get the values of all the given hash fields" do
+      @client.hset("key1", "i1", "val1")
+      @client.hset("key1", "i2", "val2")
+
+      @client.hmget("key1", "i1", "i2", "i3").should =~ ["val1", "val2", nil]
+      @client.hmget("key1", ["i1", "i2", "i3"]).should =~ ["val1", "val2", nil]
+
+      @client.hmget("key2", "i1", "i2").should be == [nil, nil]
+    end
+
+    it "should throw an argument error when you don't ask for any keys" do
+      lambda { @client.hmget("key1") }.should raise_error(Redis::CommandError)
+    end
+
+    it "should reject an empty list of values" do
+      lambda { @client.hmset("key") }.should raise_error(Redis::CommandError)
+      @client.exists("key").should be false
+    end
+
+    it "rejects an insert with a key but no value" do
+      lambda { @client.hmset("key", 'foo') }.should raise_error(Redis::CommandError)
+      lambda { @client.hmset("key", 'foo', 3, 'bar') }.should raise_error(Redis::CommandError)
+      @client.exists("key").should be false
+    end
+
+    it "should reject the wrong number of arguments" do
+      lambda { @client.hmset("hash", "foo1", "bar1", "foo2", "bar2", "foo3") }.should raise_error(Redis::CommandError, "ERR wrong number of arguments for HMSET")
+    end
+
+    it "should set multiple hash fields to multiple values" do
+      @client.hmset("key", "k1", "value1", "k2", "value2")
+
+      @client.hget("key", "k1").should be == "value1"
+      @client.hget("key", "k2").should be == "value2"
+    end
+
+    it "should set multiple hash fields from a ruby hash to multiple values" do
+      @client.mapped_hmset("foo", :k1 => "value1", :k2 => "value2")
+
+      @client.hget("foo", "k1").should be == "value1"
+      @client.hget("foo", "k2").should be == "value2"
+    end
+
+    it "should set the string value of a hash field" do
+      @client.hset("key1", "k1", "val1").should be == true
+      @client.hset("key1", "k1", "val1").should be == false
+
+      @client.hget("key1", "k1").should be == "val1"
+    end
+
+    it "should set the value of a hash field, only if the field does not exist" do
+      @client.hset("key1", "k1", "val1")
+      @client.hsetnx("key1", "k1", "value").should be == false
+      @client.hsetnx("key1", "k2", "val2").should be == true
+      @client.hsetnx("key1", :k1, "value").should be == false
+      @client.hsetnx("key1", :k3, "val3").should be == true
+
+      @client.hget("key1", "k1").should be == "val1"
+      @client.hget("key1", "k2").should be == "val2"
+      @client.hget("key1", "k3").should be == "val3"
+    end
+
+    it "should get all the values in a hash" do
+      @client.hset("key1", "k1", "val1")
+      @client.hset("key1", "k2", "val2")
+
+      @client.hvals("key1").should =~ ["val1", "val2"]
+    end
+
+    it "should accept a list of array pairs as arguments and not throw an invalid argument number error" do
+      @client.hmset("key1", [:k1, "val1"], [:k2, "val2"], [:k3, "val3"])
+      @client.hget("key1", :k1).should be == "val1"
+      @client.hget("key1", :k2).should be == "val2"
+      @client.hget("key1", :k3).should be == "val3"
+    end
+
+    it "should reject a list of arrays that contain an invalid number of arguments" do
+      expect { @client.hmset("key1", [:k1, "val1"], [:k2, "val2", "bogus val"]) }.to raise_error(Redis::CommandError, "ERR wrong number of arguments for HMSET")
+    end
+
+    it "should convert a integer field name to string for hdel" do
+      @client.hset("key1", "1", 1)
+      @client.hdel("key1", 1).should be(1)
+    end
+
+    it "should convert a integer field name to string for hexists" do
+      @client.hset("key1", "1", 1)
+      @client.hexists("key1", 1).should be true
+    end
+
+    it "should convert a integer field name to string for hincrby" do
+      @client.hset("key1", 1, 0)
+      @client.hincrby("key1", 1, 1).should be(1)
+    end
+  end
+end
diff --git a/spec/keys_spec.rb b/spec/keys_spec.rb
new file mode 100644
index 0000000..d54d669
--- /dev/null
+++ b/spec/keys_spec.rb
@@ -0,0 +1,371 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "KeysMethods" do
+
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should delete a key" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+      @client.del("key1", "key2")
+
+      @client.get("key1").should be == nil
+    end
+
+    it "should delete multiple keys" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+      @client.del(["key1", "key2"])
+
+      @client.get("key1").should be == nil
+      @client.get("key2").should be == nil
+    end
+
+    it "should error deleting no keys" do
+      lambda { @client.del }.should raise_error(Redis::CommandError, "ERR wrong number of arguments for 'del' command")
+      lambda { @client.del [] }.should raise_error(Redis::CommandError, "ERR wrong number of arguments for 'del' command")
+    end
+
+    it "should return true when setting expires on keys that exist" do
+      @client.set("key1", "1")
+      @client.expire("key1", 1).should == true
+    end
+
+    it "should return false when attempting to set expires on a key that does not exist" do
+      @client.expire("key1", 1).should == false
+    end
+
+    it "should determine if a key exists" do
+      @client.set("key1", "1")
+
+      @client.exists("key1").should be == true
+      @client.exists("key2").should be == false
+    end
+
+    it "should set a key's time to live in seconds" do
+      @client.set("key1", "1")
+      @client.expire("key1", 1)
+
+      @client.ttl("key1").should be == 1
+    end
+
+    it "should set the expiration for a key as a UNIX timestamp" do
+      @client.set("key1", "1")
+      @client.expireat("key1", Time.now.to_i + 2)
+
+      @client.ttl("key1").should be == 2
+    end
+
+    it "should not have an expiration after re-set" do
+      @client.set("key1", "1")
+      @client.expireat("key1", Time.now.to_i + 2)
+      @client.set("key1", "1")
+
+      @client.ttl("key1").should be == -1
+    end
+
+    it "should not have a ttl if expired (and thus key does not exist)" do
+      @client.set("key1", "1")
+      @client.expireat("key1", Time.now.to_i)
+
+      @client.ttl("key1").should be == -2
+    end
+
+    it "should not find a key if expired" do
+      @client.set("key1", "1")
+      @client.expireat("key1", Time.now.to_i)
+
+      @client.get("key1").should be_nil
+    end
+
+    it "should not find multiple keys if expired" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+      @client.expireat("key1", Time.now.to_i)
+
+      @client.mget("key1", "key2").should be == [nil, "2"]
+    end
+
+    it "should only find keys that aren't expired" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+      @client.expireat("key1", Time.now.to_i)
+
+      @client.keys.should be == ["key2"]
+    end
+
+    it "should not exist if expired" do
+      @client.set("key1", "1")
+      @client.expireat("key1", Time.now.to_i)
+
+      @client.exists("key1").should be false
+    end
+
+    it "should find all keys matching the given pattern" do
+      @client.set("key:a", "1")
+      @client.set("key:b", "2")
+      @client.set("key:c", "3")
+      @client.set("akeyd", "4")
+      @client.set("key1", "5")
+
+      @client.mset("database", 1, "above", 2, "suitability", 3, "able", 4)
+
+      @client.keys("key:*").should =~ ["key:a", "key:b", "key:c"]
+      @client.keys("ab*").should =~ ["above", "able"]
+    end
+
+    it "should remove the expiration from a key" do
+      @client.set("key1", "1")
+      @client.expireat("key1", Time.now.to_i + 1)
+      @client.persist("key1").should be == true
+      @client.persist("key1").should be == false
+
+      @client.ttl("key1").should be == -1
+    end
+
+    it "should return a random key from the keyspace" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+
+      ["key1", "key2"].include?(@client.randomkey).should be == true
+    end
+
+    it "should rename a key" do
+      @client.set("key1", "2")
+      @client.rename("key1", "key2")
+
+      @client.get("key1").should be == nil
+      @client.get("key2").should be == "2"
+    end
+
+    it "should rename a key, only if new key does not exist" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+      @client.set("key3", "3")
+      @client.renamenx("key1", "key2")
+      @client.renamenx("key3", "key4")
+
+      @client.get("key1").should be == "1"
+      @client.get("key2").should be == "2"
+      @client.get("key3").should be == nil
+      @client.get("key4").should be == "3"
+    end
+
+    it "should determine the type stored at key" do
+      # Non-existing key
+      @client.type("key0").should be == "none"
+
+      # String
+      @client.set("key1", "1")
+      @client.type("key1").should be == "string"
+
+      # List
+      @client.lpush("key2", "1")
+      @client.type("key2").should be == "list"
+
+      # Set
+      @client.sadd("key3", "1")
+      @client.type("key3").should be == "set"
+
+      # Sorted Set
+      @client.zadd("key4", 1.0, "1")
+      @client.type("key4").should be == "zset"
+
+      # Hash
+      @client.hset("key5", "a", "1")
+      @client.type("key5").should be == "hash"
+    end
+
+    it "should convert the value into a string before storing" do
+      @client.set("key1", 1)
+      @client.get("key1").should be == "1"
+
+      @client.setex("key2", 30, 1)
+      @client.get("key2").should be == "1"
+
+      @client.getset("key3", 1)
+      @client.get("key3").should be == "1"
+    end
+
+    it "should return 'OK' for the setex command" do
+      @client.setex("key4", 30, 1).should be == "OK"
+    end
+
+    it "should convert the key into a string before storing" do
+      @client.set(123, "foo")
+      @client.keys.should include("123")
+      @client.get("123").should be == "foo"
+
+      @client.setex(456, 30, "foo")
+      @client.keys.should include("456")
+      @client.get("456").should be == "foo"
+
+      @client.getset(789, "foo")
+      @client.keys.should include("789")
+      @client.get("789").should be == "foo"
+    end
+
+    it "should only operate against keys containing string values" do
+      @client.sadd("key1", "one")
+      lambda { @client.get("key1") }.should raise_error(Redis::CommandError, "WRONGTYPE Operation against a key holding the wrong kind of value")
+      lambda { @client.getset("key1", 1) }.should raise_error(Redis::CommandError, "WRONGTYPE Operation against a key holding the wrong kind of value")
+
+      @client.hset("key2", "one", "two")
+      lambda { @client.get("key2") }.should raise_error(Redis::CommandError, "WRONGTYPE Operation against a key holding the wrong kind of value")
+      lambda { @client.getset("key2", 1) }.should raise_error(Redis::CommandError, "WRONGTYPE Operation against a key holding the wrong kind of value")
+    end
+
+    it "should move a key from one database to another successfully" do
+      @client.select(0)
+      @client.set("key1", "1")
+
+      @client.move("key1", 1).should be == true
+
+      @client.select(0)
+      @client.get("key1").should be_nil
+
+      @client.select(1)
+      @client.get("key1").should be == "1"
+    end
+
+    it "should fail to move a key that does not exist in the source database" do
+      @client.select(0)
+      @client.get("key1").should be_nil
+
+      @client.move("key1", 1).should be == false
+
+      @client.select(0)
+      @client.get("key1").should be_nil
+
+      @client.select(1)
+      @client.get("key1").should be_nil
+    end
+
+    it "should fail to move a key that exists in the destination database" do
+      @client.select(0)
+      @client.set("key1", "1")
+
+      @client.select(1)
+      @client.set("key1", "2")
+
+      @client.select(0)
+      @client.move("key1", 1).should be == false
+
+      @client.select(0)
+      @client.get("key1").should be == "1"
+
+      @client.select(1)
+      @client.get("key1").should be == "2"
+    end
+
+    it "should fail to move a key to the same database" do
+      @client.select(0)
+      @client.set("key1", "1")
+
+      lambda { @client.move("key1", 0) }.should raise_error(Redis::CommandError, "ERR source and destination objects are the same")
+
+      @client.select(0)
+      @client.get("key1").should be == "1"
+    end
+
+    it "should scan all keys in the database" do
+      100.times do |x|
+        @client.set("key#{x}", "#{x}")
+      end
+
+      cursor = 0
+      all_keys = []
+      loop {
+        cursor, keys = @client.scan(cursor)
+        all_keys += keys
+        break if cursor == "0"
+      }
+
+      all_keys.uniq.size.should == 100
+      all_keys[0].should =~ /key\d+/
+    end
+
+    it "should match keys to a pattern when scanning" do
+      50.times do |x|
+        @client.set("key#{x}", "#{x}")
+      end
+
+      @client.set("miss_me", 1)
+      @client.set("pass_me", 2)
+
+      cursor = 0
+      all_keys = []
+      loop {
+        cursor, keys = @client.scan(cursor, :match => "key*")
+        all_keys += keys
+        break if cursor == "0"
+      }
+
+      all_keys.uniq.size.should == 50
+    end
+
+    it "should specify doing more work when scanning" do
+      100.times do |x|
+        @client.set("key#{x}", "#{x}")
+      end
+
+      cursor, all_keys = @client.scan(cursor, :count => 100)
+
+      cursor.should == "0"
+      all_keys.uniq.size.should == 100
+    end
+
+    context "with extended options" do
+      it "uses ex option to set the expire time, in seconds" do
+        ttl = 7
+
+        @client.set("key1", "1", { :ex => ttl }).should == "OK"
+        @client.ttl("key1").should == ttl
+      end
+
+      it "uses px option to set the expire time, in miliseconds" do
+        ttl = 7000
+
+        @client.set("key1", "1", { :px => ttl }).should == "OK"
+        @client.ttl("key1").should == (ttl / 1000)
+      end
+
+      # Note that the redis-rb implementation will always give PX last.
+      # Redis seems to process each expiration option and the last one wins.
+      it "prefers the finer-grained PX expiration option over EX" do
+        ttl_px = 6000
+        ttl_ex = 10
+
+        @client.set("key1", "1", { :px => ttl_px, :ex => ttl_ex })
+        @client.ttl("key1").should == (ttl_px / 1000)
+
+        @client.set("key1", "1", { :ex => ttl_ex, :px => ttl_px })
+        @client.ttl("key1").should == (ttl_px / 1000)
+      end
+
+      it "uses nx option to only set the key if it does not already exist" do
+        @client.set("key1", "1", { :nx => true }).should == true
+        @client.set("key1", "2", { :nx => true }).should == false
+
+        @client.get("key1").should == "1"
+      end
+
+      it "uses xx option to only set the key if it already exists" do
+        @client.set("key2", "1", { :xx => true }).should == false
+        @client.set("key2", "2")
+        @client.set("key2", "1", { :xx => true }).should == true
+
+        @client.get("key2").should == "1"
+      end
+
+      it "does not set the key if both xx and nx option are specified" do
+        @client.set("key2", "1", { :nx => true, :xx => true }).should == false
+        @client.get("key2").should be_nil
+      end
+    end
+  end
+end
+
diff --git a/spec/lists_spec.rb b/spec/lists_spec.rb
new file mode 100644
index 0000000..41f9852
--- /dev/null
+++ b/spec/lists_spec.rb
@@ -0,0 +1,203 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "ListsMethods" do
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should get an element from a list by its index" do
+      @client.lpush("key1", "val1")
+      @client.lpush("key1", "val2")
+
+      @client.lindex("key1", 0).should be == "val2"
+      @client.lindex("key1", -1).should be == "val1"
+      @client.lindex("key1", 3).should be == nil
+    end
+
+    it "should insert an element before or after another element in a list" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v3")
+      @client.linsert("key1", :before, "v3", "v2")
+
+      @client.lrange("key1", 0, -1).should be == ["v1", "v2", "v3"]
+    end
+
+    it 'should allow multiple values to be added to a list in a single rpush' do
+      @client.rpush('key1', [1, 2, 3])
+      @client.lrange('key1', 0, -1).should be == ['1', '2', '3']
+    end
+
+    it 'should allow multiple values to be added to a list in a single lpush' do
+      @client.lpush('key1', [1, 2, 3])
+      @client.lrange('key1', 0, -1).should be == ['3', '2', '1']
+    end
+
+    it "should error if an invalid where argument is given" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v3")
+      lambda { @client.linsert("key1", :invalid, "v3", "v2") }.should raise_error(Redis::CommandError, "ERR syntax error")
+    end
+
+    it "should get the length of a list" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v2")
+
+      @client.llen("key1").should be == 2
+      @client.llen("key2").should be == 0
+    end
+
+    it "should remove and get the first element in a list" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v2")
+      @client.rpush("key1", "v3")
+
+      @client.lpop("key1").should be == "v1"
+      @client.lrange("key1", 0, -1).should be == ["v2", "v3"]
+    end
+
+    it "should prepend a value to a list" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v2")
+
+      @client.lrange("key1", 0, -1).should be == ["v1", "v2"]
+    end
+
+    it "should prepend a value to a list, only if the list exists" do
+      @client.lpush("key1", "v1")
+
+      @client.lpushx("key1", "v2")
+      @client.lpushx("key2", "v3")
+
+      @client.lrange("key1", 0, -1).should be == ["v2", "v1"]
+      @client.llen("key2").should be == 0
+    end
+
+    it "should get a range of elements from a list" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v2")
+      @client.rpush("key1", "v3")
+
+      @client.lrange("key1", 1, -1).should be == ["v2", "v3"]
+    end
+
+    it "should remove elements from a list" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v2")
+      @client.rpush("key1", "v2")
+      @client.rpush("key1", "v2")
+      @client.rpush("key1", "v1")
+
+      @client.lrem("key1", 1, "v1").should be == 1
+      @client.lrem("key1", -2, "v2").should be == 2
+      @client.llen("key1").should be == 2
+    end
+
+    it "should remove list's key when list is empty" do
+      @client.rpush("key1", "v1")
+      @client.rpush("key1", "v2")
+      @client.lrem("key1", 1, "v1")
+      @client.lrem("key1", 1, "v2")
+
+      @client.exists("key1").should be == false
+    end
+
+    it "should set the value of an element in a list by its index" do
+      @client.rpush("key1", "one")
+      @client.rpush("key1", "two")
+      @client.rpush("key1", "three")
+
+      @client.lset("key1", 0, "four")
+      @client.lset("key1", -2, "five")
+      @client.lrange("key1", 0, -1).should be == ["four", "five", "three"]
+
+      lambda { @client.lset("key1", 4, "six") }.should raise_error(Redis::CommandError, "ERR index out of range")
+    end
+
+    it "should trim a list to the specified range" do
+      @client.rpush("key1", "one")
+      @client.rpush("key1", "two")
+      @client.rpush("key1", "three")
+
+      @client.ltrim("key1", 1, -1).should be == "OK"
+      @client.lrange("key1", 0, -1).should be == ["two", "three"]
+    end
+
+
+    context "when the list is smaller than the requested trim" do
+      before { @client.rpush("listOfOne", "one") }
+
+      context "trimming with a negative start (specifying a max)" do
+        before { @client.ltrim("listOfOne", -5, -1) }
+
+        it "returns the unmodified list" do
+          @client.lrange("listOfOne", 0, -1).should be == ["one"]
+        end
+      end
+    end
+
+    context "when the list is larger than the requested trim" do
+      before do
+        @client.rpush("maxTest", "one")
+        @client.rpush("maxTest", "two")
+        @client.rpush("maxTest", "three")
+        @client.rpush("maxTest", "four")
+        @client.rpush("maxTest", "five")
+        @client.rpush("maxTest", "six")
+      end
+
+      context "trimming with a negative start (specifying a max)" do
+        before { @client.ltrim("maxTest", -5, -1) }
+
+        it "should trim a list to the specified maximum size" do
+          @client.lrange("maxTest", 0, -1).should be == ["two","three", "four", "five", "six"]
+        end
+      end
+    end
+
+
+    it "should remove and return the last element in a list" do
+      @client.rpush("key1", "one")
+      @client.rpush("key1", "two")
+      @client.rpush("key1", "three")
+
+      @client.rpop("key1").should be == "three"
+      @client.lrange("key1", 0, -1).should be == ["one", "two"]
+    end
+
+    it "should remove the last element in a list, append it to another list and return it" do
+      @client.rpush("key1", "one")
+      @client.rpush("key1", "two")
+      @client.rpush("key1", "three")
+
+      @client.rpoplpush("key1", "key2").should be == "three"
+
+      @client.lrange("key1", 0, -1).should be == ["one", "two"]
+      @client.lrange("key2", 0, -1).should be == ["three"]
+    end
+
+    context 'when the source list is empty' do
+      it 'rpoplpush does not add anything to the destination list' do
+        @client.rpoplpush("source", "destination")
+
+        @client.lrange("destination", 0, -1).should be == []
+      end
+    end
+
+    it "should append a value to a list" do
+      @client.rpush("key1", "one")
+      @client.rpush("key1", "two")
+
+      @client.lrange("key1", 0, -1).should be == ["one", "two"]
+    end
+
+    it "should append a value to a list, only if the list exists" do
+      @client.rpush("key1", "one")
+      @client.rpushx("key1", "two")
+      @client.rpushx("key2", "two")
+
+      @client.lrange("key1", 0, -1).should be == ["one", "two"]
+      @client.lrange("key2", 0, -1).should be == []
+    end
+  end
+end
diff --git a/spec/memory_spec.rb b/spec/memory_spec.rb
new file mode 100644
index 0000000..7d179ed
--- /dev/null
+++ b/spec/memory_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe 'time' do
+    before(:each) do
+      @client = Redis.new
+      Time.stub_chain(:now, :to_f).and_return(1397845595.5139461)
+    end
+
+    it 'is an array' do
+      expect(@client.time).to be_an_instance_of(Array)
+    end
+
+    it 'has two elements' do
+      expect(@client.time.count).to eql 2
+    end
+
+    if fakeredis?
+      it 'has the current time in seconds' do
+        expect(@client.time.first).to eql 1397845595
+      end
+
+      it 'has the current leftover microseconds' do
+        expect(@client.time.last).to eql 513946
+      end
+    end
+  end
+end
diff --git a/spec/server_spec.rb b/spec/server_spec.rb
new file mode 100644
index 0000000..7a8cc71
--- /dev/null
+++ b/spec/server_spec.rb
@@ -0,0 +1,100 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "ServerMethods" do
+
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should return the number of keys in the selected database" do
+      @client.set("key1", "1")
+      @client.set("key2", "2")
+      @client.set("key2", "two")
+
+      @client.dbsize.should be == 2
+    end
+
+    it "should get information and statistics about the server" do
+      @client.info.key?("redis_version").should be == true
+    end
+
+    it "should handle non-existent methods" do
+      lambda { @client.idontexist }.should raise_error(Redis::CommandError, "ERR unknown command 'idontexist'")
+    end
+
+    describe "multiple databases" do
+      it "should default to database 0" do
+        @client.inspect.should =~ %r#/0>$#
+      end
+
+      it "should select another database" do
+        @client.select(1)
+        @client.inspect.should =~ %r#/1>$#
+      end
+
+      it "should store keys separately in each database" do
+        @client.select(0).should be == "OK"
+        @client.set("key1", "1")
+        @client.set("key2", "2")
+
+        @client.select(1)
+        @client.set("key3", "3")
+        @client.set("key4", "4")
+        @client.set("key5", "5")
+
+        @client.select(0)
+        @client.dbsize.should be == 2
+        @client.exists("key1").should be true
+        @client.exists("key3").should be false
+
+        @client.select(1)
+        @client.dbsize.should be == 3
+        @client.exists("key4").should be true
+        @client.exists("key2").should be false
+
+        @client.flushall
+        @client.dbsize.should be == 0
+
+        @client.select(0)
+        @client.dbsize.should be == 0
+      end
+
+      it "should flush a database" do
+        @client.select(0)
+        @client.set("key1", "1")
+        @client.set("key2", "2")
+        @client.dbsize.should be == 2
+
+        @client.select(1)
+        @client.set("key3", "3")
+        @client.set("key4", "4")
+        @client.dbsize.should be == 2
+
+        @client.flushdb.should be == "OK"
+
+        @client.dbsize.should be == 0
+        @client.select(0)
+        @client.dbsize.should be == 2
+      end
+
+      it "should flush all databases" do
+        @client.select(0)
+        @client.set("key3", "3")
+        @client.set("key4", "4")
+        @client.dbsize.should be == 2
+
+        @client.select(1)
+        @client.set("key3", "3")
+        @client.set("key4", "4")
+        @client.dbsize.should be == 2
+
+        @client.flushall.should be == "OK"
+
+        @client.dbsize.should be == 0
+        @client.select(0)
+        @client.dbsize.should be == 0
+      end
+    end
+  end
+end
diff --git a/spec/sets_spec.rb b/spec/sets_spec.rb
new file mode 100644
index 0000000..a1851d6
--- /dev/null
+++ b/spec/sets_spec.rb
@@ -0,0 +1,269 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "SetsMethods" do
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should add a member to a set" do
+      @client.sadd("key", "value").should be == true
+      @client.sadd("key", "value").should be == false
+
+      @client.smembers("key").should be == ["value"]
+    end
+
+    it "should raise error if command arguments count is not enough" do
+      expect { @client.sadd("key", []) }.to raise_error(Redis::CommandError, "ERR wrong number of arguments for 'sadd' command")
+      expect { @client.sinter(*[]) }.to raise_error(Redis::CommandError, "ERR wrong number of arguments for 'sinter' command")
+
+      @client.smembers("key").should be_empty
+    end
+
+    it "should add multiple members to a set" do
+      @client.sadd("key", %w(value other something more)).should be == 4
+      @client.sadd("key", %w(and additional values)).should be == 3
+      @client.smembers("key").should =~ ["value", "other", "something", "more", "and", "additional", "values"]
+    end
+
+    it "should get the number of members in a set" do
+      @client.sadd("key", "val1")
+      @client.sadd("key", "val2")
+
+      @client.scard("key").should be == 2
+    end
+
+    it "should subtract multiple sets" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key1", "c")
+      @client.sadd("key1", "d")
+      @client.sadd("key2", "c")
+      @client.sadd("key3", "a")
+      @client.sadd("key3", "c")
+      @client.sadd("key3", "e")
+
+      @client.sdiff("key1", "key2", "key3").should =~ ["b", "d"]
+    end
+
+    it "should subtract from a nonexistent set" do
+      @client.sadd("key2", "a")
+      @client.sadd("key2", "b")
+
+      @client.sdiff("key1", "key2").should == []
+    end
+
+    it "should subtract multiple sets and store the resulting set in a key" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key1", "c")
+      @client.sadd("key1", "d")
+      @client.sadd("key2", "c")
+      @client.sadd("key3", "a")
+      @client.sadd("key3", "c")
+      @client.sadd("key3", "e")
+      @client.sdiffstore("key", "key1", "key2", "key3")
+
+      @client.smembers("key").should =~ ["b", "d"]
+    end
+
+    it "should intersect multiple sets" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key1", "c")
+      @client.sadd("key1", "d")
+      @client.sadd("key2", "c")
+      @client.sadd("key3", "a")
+      @client.sadd("key3", "c")
+      @client.sadd("key3", "e")
+
+      @client.sinter("key1", "key2", "key3").should be == ["c"]
+    end
+
+    it "should intersect multiple sets and store the resulting set in a key" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key1", "c")
+      @client.sadd("key1", "d")
+      @client.sadd("key2", "c")
+      @client.sadd("key3", "a")
+      @client.sadd("key3", "c")
+      @client.sadd("key3", "e")
+      @client.sinterstore("key", "key1", "key2", "key3")
+      @client.smembers("key").should be == ["c"]
+    end
+
+    it "should determine if a given value is a member of a set" do
+      @client.sadd("key1", "a")
+
+      @client.sismember("key1", "a").should be == true
+      @client.sismember("key1", "b").should be == false
+      @client.sismember("key2", "a").should be == false
+    end
+
+    it "should get all the members in a set" do
+      @client.sadd("key", "a")
+      @client.sadd("key", "b")
+      @client.sadd("key", "c")
+      @client.sadd("key", "d")
+
+      @client.smembers("key").should =~ ["a", "b", "c", "d"]
+    end
+
+    it "should move a member from one set to another" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key2", "c")
+      @client.smove("key1", "key2", "a").should be == true
+      @client.smove("key1", "key2", "a").should be == false
+
+      @client.smembers("key1").should be == ["b"]
+      @client.smembers("key2").should =~ ["c", "a"]
+    end
+
+    it "should remove and return a random member from a set" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+
+      ["a", "b"].include?(@client.spop("key1")).should be true
+      ["a", "b"].include?(@client.spop("key1")).should be true
+      @client.spop("key1").should be_nil
+    end
+
+    it "should get a random member from a set" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+
+      ["a", "b"].include?(@client.spop("key1")).should be true
+    end
+
+    it "should remove a member from a set" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.srem("key1", "a").should be == true
+      @client.srem("key1", "a").should be == false
+
+      @client.smembers("key1").should be == ["b"]
+    end
+
+    it "should remove multiple members from a set" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+
+      @client.srem("key1", [ "a", "b"]).should == 2
+      @client.smembers("key1").should be_empty
+    end
+
+    it "should remove the set's key once it's empty" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.srem("key1", "b")
+      @client.srem("key1", "a")
+
+      @client.exists("key1").should be == false
+    end
+
+    it "should add multiple sets" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key1", "c")
+      @client.sadd("key1", "d")
+      @client.sadd("key2", "c")
+      @client.sadd("key3", "a")
+      @client.sadd("key3", "c")
+      @client.sadd("key3", "e")
+
+      @client.sunion("key1", "key2", "key3").should =~ ["a", "b", "c", "d", "e"]
+    end
+
+    it "should add multiple sets and store the resulting set in a key" do
+      @client.sadd("key1", "a")
+      @client.sadd("key1", "b")
+      @client.sadd("key1", "c")
+      @client.sadd("key1", "d")
+      @client.sadd("key2", "c")
+      @client.sadd("key3", "a")
+      @client.sadd("key3", "c")
+      @client.sadd("key3", "e")
+      @client.sunionstore("key", "key1", "key2", "key3")
+
+      @client.smembers("key").should =~ ["a", "b", "c", "d", "e"]
+    end
+  end
+
+  describe 'srandmember' do
+    before(:each) do
+      @client = Redis.new
+    end
+
+    context 'with a set that has three elements' do
+      before do
+        @client.sadd("key1", "a")
+        @client.sadd("key1", "b")
+        @client.sadd("key1", "c")
+      end
+
+      context 'when called without the optional number parameter' do
+        it 'is a random element from the set' do
+          random_element = @client.srandmember("key1")
+
+          ['a', 'b', 'c'].include?(random_element).should be true
+        end
+      end
+
+      context 'when called with the optional number parameter of 1' do
+        it 'is an array of one random element from the set' do
+          random_elements = @client.srandmember("key1", 1)
+
+          [['a'], ['b'], ['c']].include?(@client.srandmember("key1", 1)).should be true
+        end
+      end
+
+      context 'when called with the optional number parameter of 2' do
+        it 'is an array of two unique, random elements from the set' do
+          random_elements = @client.srandmember("key1", 2)
+
+          random_elements.count.should == 2
+          random_elements.uniq.count.should == 2
+          random_elements.all? do |element|
+            ['a', 'b', 'c'].include?(element).should be true
+          end
+        end
+      end
+
+      context 'when called with an optional parameter of -100' do
+        it 'is an array of 100 random elements from the set, some of which are repeated' do
+          random_elements = @client.srandmember("key1", -100)
+
+          random_elements.count.should == 100
+          random_elements.uniq.count.should <= 3
+          random_elements.all? do |element|
+            ['a', 'b', 'c'].include?(element).should be true
+          end
+        end
+      end
+
+      context 'when called with an optional parameter of 100' do
+        it 'is an array of all of the elements from the set, none of which are repeated' do
+          random_elements = @client.srandmember("key1", 100)
+
+          random_elements.count.should == 3
+          random_elements.uniq.count.should == 3
+          random_elements.should =~ ['a', 'b', 'c']
+        end
+      end
+    end
+
+    context 'with an empty set' do
+      before { @client.del("key1") }
+
+      it 'is nil without the extra parameter' do
+        @client.srandmember("key1").should be_nil
+      end
+
+      it 'is an empty array with an extra parameter' do
+        @client.srandmember("key1", 1).should == []
+      end
+    end
+  end
+end
diff --git a/spec/sort_method_spec.rb b/spec/sort_method_spec.rb
new file mode 100644
index 0000000..771bb9c
--- /dev/null
+++ b/spec/sort_method_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "#sort" do
+    before(:each) do
+      @client = Redis.new
+
+      @client.set('fake-redis-test:values_1', 'a')
+      @client.set('fake-redis-test:values_2', 'b')
+
+      @client.set('fake-redis-test:weight_1', '2')
+      @client.set('fake-redis-test:weight_2', '1')
+
+      @client.hset('fake-redis-test:hash_1', 'key', 'x')
+      @client.hset('fake-redis-test:hash_2', 'key', 'y')
+    end
+
+    context "WRONGTYPE Operation" do
+      it "should not allow #sort on Strings" do
+        @client.set("key1", "Hello")
+        expect {
+          @client.sort("key1")
+        }.to raise_error(Redis::CommandError)
+      end
+
+      it "should not allow #sort on Hashes" do
+        @client.hset("key1", "k1", "val1")
+        @client.hset("key1", "k2", "val2")
+        expect {
+          @client.sort("key1")
+        }.to raise_error(Redis::CommandError)
+      end
+    end
+
+    context "list" do
+      before do
+        @key = "fake-redis-test:list_sort"
+
+        @client.rpush(@key, '1')
+        @client.rpush(@key, '2')
+      end
+
+      it_should_behave_like "a sortable"
+    end
+
+    context "set" do
+      before do
+        @key = "fake-redis-test:set_sort"
+
+        @client.sadd(@key, '1')
+        @client.sadd(@key, '2')
+      end
+
+      it_should_behave_like "a sortable"
+    end
+
+    context "zset" do
+      before do
+        @key = "fake-redis-test:zset_sort"
+
+        @client.zadd(@key, 100, '1')
+        @client.zadd(@key, 99, '2')
+      end
+
+      it_should_behave_like "a sortable"
+    end
+  end
+end
diff --git a/spec/sorted_sets_spec.rb b/spec/sorted_sets_spec.rb
new file mode 100644
index 0000000..631e2b1
--- /dev/null
+++ b/spec/sorted_sets_spec.rb
@@ -0,0 +1,440 @@
+require 'spec_helper'
+
+module FakeRedis
+  Infinity = 1.0/0.0
+
+  describe "SortedSetsMethods" do
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should add a member to a sorted set, or update its score if it already exists" do
+      @client.zadd("key", 1, "val").should be(true)
+      @client.zscore("key", "val").should be == 1.0
+
+      @client.zadd("key", 2, "val").should be(false)
+      @client.zscore("key", "val").should be == 2.0
+
+      # These assertions only work in redis-rb v3.0.2 or higher
+      @client.zadd("key2", "inf", "val").should be == true
+      @client.zscore("key2", "val").should be == Infinity
+
+      @client.zadd("key3", "+inf", "val").should be == true
+      @client.zscore("key3", "val").should be == Infinity
+
+      @client.zadd("key4", "-inf", "val").should be == true
+      @client.zscore("key4", "val").should be == -Infinity
+    end
+
+    it "should return a nil score for value not in a sorted set or empty key" do
+      @client.zadd "key", 1, "val"
+
+      @client.zscore("key", "val").should be == 1.0
+      @client.zscore("key", "val2").should be_nil
+      @client.zscore("key2", "val").should be_nil
+    end
+
+    it "should add multiple members to a sorted set, or update its score if it already exists" do
+      @client.zadd("key", [1, "val", 2, "val2"]).should be == 2
+      @client.zscore("key", "val").should be == 1
+      @client.zscore("key", "val2").should be == 2
+
+      @client.zadd("key", [[5, "val"], [3, "val3"], [4, "val4"]]).should be == 2
+      @client.zscore("key", "val").should be == 5
+      @client.zscore("key", "val2").should be == 2
+      @client.zscore("key", "val3").should be == 3
+      @client.zscore("key", "val4").should be == 4
+
+      @client.zadd("key", [[5, "val5"], [3, "val6"]]).should be == 2
+      @client.zscore("key", "val5").should be == 5
+      @client.zscore("key", "val6").should be == 3
+    end
+
+    it "should error with wrong number of arguments when adding members" do
+      lambda { @client.zadd("key") }.should raise_error(ArgumentError, "wrong number of arguments")
+      lambda { @client.zadd("key", 1) }.should raise_error(ArgumentError, "wrong number of arguments")
+      lambda { @client.zadd("key", [1]) }.should raise_error(Redis::CommandError, "ERR wrong number of arguments for 'zadd' command")
+      lambda { @client.zadd("key", [1, "val", 2]) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      lambda { @client.zadd("key", [[1, "val"], [2]]) }.should raise_error(Redis::CommandError, "ERR syntax error")
+    end
+
+    it "should allow floats as scores when adding or updating" do
+      @client.zadd("key", 4.321, "val").should be(true)
+      @client.zscore("key", "val").should be == 4.321
+
+      @client.zadd("key", 54.3210, "val").should be(false)
+      @client.zscore("key", "val").should be == 54.321
+    end
+
+    it "should remove members from sorted sets" do
+      @client.zrem("key", "val").should be(false)
+      @client.zadd("key", 1, "val").should be(true)
+      @client.zrem("key", "val").should be(true)
+    end
+
+    it "should remove multiple members from sorted sets" do
+      @client.zrem("key2", %w(val)).should be == 0
+      @client.zrem("key2", %w(val val2 val3)).should be == 0
+
+      @client.zadd("key2", 1, "val")
+      @client.zadd("key2", 1, "val2")
+      @client.zadd("key2", 1, "val3")
+
+      @client.zrem("key2", %w(val val2 val3 val4)).should be == 3
+    end
+
+    it "should remove sorted set's key when it is empty" do
+      @client.zadd("key", 1, "val")
+      @client.zrem("key", "val")
+      @client.exists("key").should be == false
+    end
+
+    it "should get the number of members in a sorted set" do
+      @client.zadd("key", 1, "val2")
+      @client.zadd("key", 2, "val1")
+      @client.zadd("key", 5, "val3")
+
+      @client.zcard("key").should be == 3
+    end
+
+    it "should count the members in a sorted set with scores within the given values" do
+      @client.zadd("key", 1, "val1")
+      @client.zadd("key", 2, "val2")
+      @client.zadd("key", 3, "val3")
+
+      @client.zcount("key", 2, 3).should be == 2
+    end
+
+    it "should increment the score of a member in a sorted set" do
+      @client.zadd("key", 1, "val1")
+      @client.zincrby("key", 2, "val1").should be == 3
+      @client.zscore("key", "val1").should be == 3
+    end
+
+    it "initializes the sorted set if the key wasnt already set" do
+      @client.zincrby("key", 1, "val1").should be == 1
+    end
+
+    it "should convert the key to a string for zscore" do
+      @client.zadd("key", 1, 1)
+      @client.zscore("key", 1).should be == 1
+    end
+
+    # These won't pass until redis-rb releases v3.0.2
+    it "should handle infinity values when incrementing a sorted set key" do
+      @client.zincrby("bar", "+inf", "s2").should be == Infinity
+      @client.zincrby("bar", "-inf", "s1").should be == -Infinity
+    end
+
+    it "should return a range of members in a sorted set, by index" do
+      @client.zadd("key", 1, "one")
+      @client.zadd("key", 2, "two")
+      @client.zadd("key", 3, "three")
+
+      @client.zrange("key", 0, -1).should be == ["one", "two", "three"]
+      @client.zrange("key", 1, 2).should be == ["two", "three"]
+      @client.zrange("key", 0, -1, :withscores => true).should be == [["one", 1], ["two", 2], ["three", 3]]
+      @client.zrange("key", 1, 2, :with_scores => true).should be == [["two", 2], ["three", 3]]
+    end
+
+    it "should sort zrange results logically" do
+      @client.zadd("key", 5, "val2")
+      @client.zadd("key", 5, "val3")
+      @client.zadd("key", 5, "val1")
+
+      @client.zrange("key", 0, -1).should be == %w(val1 val2 val3)
+      @client.zrange("key", 0, -1, :with_scores => true).should be == [["val1", 5], ["val2", 5], ["val3", 5]]
+    end
+
+    it "should return a reversed range of members in a sorted set, by index" do
+      @client.zadd("key", 1, "one")
+      @client.zadd("key", 2, "two")
+      @client.zadd("key", 3, "three")
+
+      @client.zrevrange("key", 0, -1).should be == ["three", "two", "one"]
+      @client.zrevrange("key", 1, 2).should be == ["two", "one"]
+      @client.zrevrange("key", 0, -1, :withscores => true).should be == [["three", 3], ["two", 2], ["one", 1]]
+      @client.zrevrange("key", 0, -1, :with_scores => true).should be == [["three", 3], ["two", 2], ["one", 1]]
+    end
+
+    it "should return a range of members in a sorted set, by score" do
+      @client.zadd("key", 1, "one")
+      @client.zadd("key", 2, "two")
+      @client.zadd("key", 3, "three")
+
+      @client.zrangebyscore("key", 0, 100).should be == ["one", "two", "three"]
+      @client.zrangebyscore("key", 1, 2).should be == ["one", "two"]
+      @client.zrangebyscore("key", 1, '(2').should be == ['one']
+      @client.zrangebyscore("key", '(1', 2).should be == ['two']
+      @client.zrangebyscore("key", '(1', '(2').should be == []
+      @client.zrangebyscore("key", 0, 100, :withscores => true).should be == [["one", 1], ["two", 2], ["three", 3]]
+      @client.zrangebyscore("key", 1, 2, :with_scores => true).should be == [["one", 1], ["two", 2]]
+      @client.zrangebyscore("key", 0, 100, :limit => [0, 1]).should be == ["one"]
+      @client.zrangebyscore("key", 0, 100, :limit => [0, -1]).should be == ["one", "two", "three"]
+      @client.zrangebyscore("key", 0, 100, :limit => [1, -1], :with_scores => true).should be == [["two", 2], ["three", 3]]
+      @client.zrangebyscore("key", '-inf', '+inf').should be == ["one", "two", "three"]
+      @client.zrangebyscore("key", 2, '+inf').should be == ["two", "three"]
+      @client.zrangebyscore("key", '-inf', 2).should be == ['one', "two"]
+
+      @client.zrangebyscore("badkey", 1, 2).should be == []
+    end
+
+    it "should return a reversed range of members in a sorted set, by score" do
+      @client.zadd("key", 1, "one")
+      @client.zadd("key", 2, "two")
+      @client.zadd("key", 3, "three")
+
+      @client.zrevrangebyscore("key", 100, 0).should be == ["three", "two", "one"]
+      @client.zrevrangebyscore("key", 2, 1).should be == ["two", "one"]
+      @client.zrevrangebyscore("key", 1, 2).should be == []
+      @client.zrevrangebyscore("key", 2, 1, :with_scores => true).should be == [["two", 2.0], ["one", 1.0]]
+      @client.zrevrangebyscore("key", 100, 0, :limit => [0, 1]).should be == ["three"]
+      @client.zrevrangebyscore("key", 100, 0, :limit => [0, -1]).should be == ["three", "two", "one"]
+      @client.zrevrangebyscore("key", 100, 0, :limit => [1, -1], :with_scores => true).should be == [["two", 2.0], ["one", 1.0]]
+    end
+
+    it "should determine the index of a member in a sorted set" do
+      @client.zadd("key", 1, "one")
+      @client.zadd("key", 2, "two")
+      @client.zadd("key", 3, "three")
+
+      @client.zrank("key", "three").should be == 2
+      @client.zrank("key", "four").should be_nil
+    end
+
+    it "should determine the reversed index of a member in a sorted set" do
+      @client.zadd("key", 1, "one")
+      @client.zadd("key", 2, "two")
+      @client.zadd("key", 3, "three")
+
+      @client.zrevrank("key", "three").should be == 0
+      @client.zrevrank("key", "four").should be_nil
+    end
+
+    it "should not raise errors for zrank() on accessing a non-existing key in a sorted set" do
+      @client.zrank("no_such_key", "no_suck_id").should be_nil
+    end
+
+    it "should not raise errors for zrevrank() on accessing a non-existing key in a sorted set" do
+      @client.zrevrank("no_such_key", "no_suck_id").should be_nil
+    end
+
+    describe "#zinterstore" do
+      before do
+        @client.zadd("key1", 1, "one")
+        @client.zadd("key1", 2, "two")
+        @client.zadd("key1", 3, "three")
+        @client.zadd("key2", 5, "two")
+        @client.zadd("key2", 7, "three")
+        @client.sadd("key3", 'one')
+        @client.sadd("key3", 'two')
+      end
+
+      it "should intersect two keys with custom scores" do
+        @client.zinterstore("out", ["key1", "key2"]).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [['two', (2 + 5)], ['three', (3 + 7)]]
+      end
+
+      it "should intersect two keys with a default score" do
+        @client.zinterstore("out", ["key1", "key3"]).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [['one', (1 + 1)], ['two', (2 + 1)]]
+      end
+
+      it "should intersect more than two keys" do
+        @client.zinterstore("out", ["key1", "key2", "key3"]).should be == 1
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [['two', (2 + 5 + 1)]]
+      end
+
+      it "should not intersect an unknown key" do
+        @client.zinterstore("out", ["key1", "no_key"]).should be == 0
+        @client.zrange("out", 0, -1, :with_scores => true).should be == []
+      end
+
+      it "should intersect two keys by minimum values" do
+        @client.zinterstore("out", ["key1", "key2"], :aggregate => :min).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["two", 2], ["three", 3]]
+      end
+
+      it "should intersect two keys by maximum values" do
+        @client.zinterstore("out", ["key1", "key2"], :aggregate => :max).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["two", 5], ["three", 7]]
+      end
+
+      it "should intersect two keys by explicitly summing values" do
+        @client.zinterstore("out", %w(key1 key2), :aggregate => :sum).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["two", (2 + 5)], ["three", (3 + 7)]]
+      end
+
+      it "should intersect two keys with weighted values" do
+        @client.zinterstore("out", %w(key1 key2), :weights => [10, 1]).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["two", (2 * 10 + 5)], ["three", (3 * 10 + 7)]]
+      end
+
+      it "should intersect two keys with weighted minimum values" do
+        @client.zinterstore("out", %w(key1 key2), :weights => [10, 1], :aggregate => :min).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["two", 5], ["three", 7]]
+      end
+
+      it "should intersect two keys with weighted maximum values" do
+        @client.zinterstore("out", %w(key1 key2), :weights => [10, 1], :aggregate => :max).should be == 2
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["two", (2 * 10)], ["three", (3 * 10)]]
+      end
+
+      it "should error without enough weights given" do
+        lambda { @client.zinterstore("out", %w(key1 key2), :weights => []) }.should raise_error(Redis::CommandError, "ERR syntax error")
+        lambda { @client.zinterstore("out", %w(key1 key2), :weights => [10]) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      end
+
+      it "should error with too many weights given" do
+        lambda { @client.zinterstore("out", %w(key1 key2), :weights => [10, 1, 1]) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      end
+
+      it "should error with an invalid aggregate" do
+        lambda { @client.zinterstore("out", %w(key1 key2), :aggregate => :invalid) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      end
+    end
+
+    context "zremrangebyscore" do
+      it "should remove items by score" do
+        @client.zadd("key", 1, "one")
+        @client.zadd("key", 2, "two")
+        @client.zadd("key", 3, "three")
+
+        @client.zremrangebyscore("key", 0, 2).should be == 2
+        @client.zcard("key").should be == 1
+      end
+
+      it "should remove items by score with infinity" do # Issue #50
+        @client.zadd("key", 10.0, "one")
+        @client.zadd("key", 20.0, "two")
+        @client.zadd("key", 30.0, "three")
+        @client.zremrangebyscore("key", "-inf", "+inf").should be == 3
+        @client.zcard("key").should be == 0
+        @client.zscore("key", "one").should be_nil
+        @client.zscore("key", "two").should be_nil
+        @client.zscore("key", "three").should be_nil
+      end
+
+      it "should return 0 if the key didn't exist" do
+        @client.zremrangebyscore("key", 0, 2).should be == 0
+      end
+    end
+
+    context '#zremrangebyrank' do
+      it 'removes all elements with in the given range' do
+        @client.zadd("key", 1, "one")
+        @client.zadd("key", 2, "two")
+        @client.zadd("key", 3, "three")
+
+        @client.zremrangebyrank("key", 0, 1).should be == 2
+        @client.zcard('key').should be == 1
+      end
+
+      it 'handles out of range requests' do
+        @client.zadd("key", 1, "one")
+        @client.zadd("key", 2, "two")
+        @client.zadd("key", 3, "three")
+
+        @client.zremrangebyrank("key", 25, -1).should be == 0
+        @client.zcard('key').should be == 3
+      end
+
+      it "should return 0 if the key didn't exist" do
+        @client.zremrangebyrank("key", 0, 1).should be == 0
+      end
+    end
+
+    describe "#zunionstore" do
+      before do
+        @client.zadd("key1", 1, "val1")
+        @client.zadd("key1", 2, "val2")
+        @client.zadd("key1", 3, "val3")
+        @client.zadd("key2", 5, "val2")
+        @client.zadd("key2", 7, "val3")
+        @client.sadd("key3", "val1")
+        @client.sadd("key3", "val2")
+      end
+
+      it "should union two keys with custom scores" do
+        @client.zunionstore("out", %w(key1 key2)).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", 1], ["val2", (2 + 5)], ["val3", (3 + 7)]]
+      end
+
+      it "should union two keys with a default score" do
+        @client.zunionstore("out", %w(key1 key3)).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", (1 + 1)], ["val2", (2 + 1)], ["val3", 3]]
+      end
+
+      it "should union more than two keys" do
+        @client.zunionstore("out", %w(key1 key2 key3)).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", (1 + 1)], ["val2", (2 + 5 + 1)], ["val3", (3 + 7)]]
+      end
+
+      it "should union with an unknown key" do
+        @client.zunionstore("out", %w(key1 no_key)).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", 1], ["val2", 2], ["val3", 3]]
+      end
+
+      it "should union two keys by minimum values" do
+        @client.zunionstore("out", %w(key1 key2), :aggregate => :min).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", 1], ["val2", 2], ["val3", 3]]
+      end
+
+      it "should union two keys by maximum values" do
+        @client.zunionstore("out", %w(key1 key2), :aggregate => :max).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", 1], ["val2", 5], ["val3", 7]]
+      end
+
+      it "should union two keys by explicitly summing values" do
+        @client.zunionstore("out", %w(key1 key2), :aggregate => :sum).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", 1], ["val2", (2 + 5)], ["val3", (3 + 7)]]
+      end
+
+      it "should union two keys with weighted values" do
+        @client.zunionstore("out", %w(key1 key2), :weights => [10, 1]).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", (1 * 10)], ["val2", (2 * 10 + 5)], ["val3", (3 * 10 + 7)]]
+      end
+
+      it "should union two keys with weighted minimum values" do
+        @client.zunionstore("out", %w(key1 key2), :weights => [10, 1], :aggregate => :min).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val2", 5], ["val3", 7], ["val1", (1 * 10)]]
+      end
+
+      it "should union two keys with weighted maximum values" do
+        @client.zunionstore("out", %w(key1 key2), :weights => [10, 1], :aggregate => :max).should be == 3
+        @client.zrange("out", 0, -1, :with_scores => true).should be == [["val1", (1 * 10)], ["val2", (2 * 10)], ["val3", (3 * 10)]]
+      end
+
+      it "should error without enough weights given" do
+        lambda { @client.zunionstore("out", %w(key1 key2), :weights => []) }.should raise_error(Redis::CommandError, "ERR syntax error")
+        lambda { @client.zunionstore("out", %w(key1 key2), :weights => [10]) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      end
+
+      it "should error with too many weights given" do
+        lambda { @client.zunionstore("out", %w(key1 key2), :weights => [10, 1, 1]) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      end
+
+      it "should error with an invalid aggregate" do
+        lambda { @client.zunionstore("out", %w(key1 key2), :aggregate => :invalid) }.should raise_error(Redis::CommandError, "ERR syntax error")
+      end
+    end
+
+    #it "should remove all members in a sorted set within the given indexes"
+
+    #it "should return a range of members in a sorted set, by index, with scores ordered from high to low"
+
+    #it "should return a range of members in a sorted set, by score, with scores ordered from high to low"
+
+    #it "should determine the index of a member in a sorted set, with scores ordered from high to low"
+
+    #it "should get the score associated with the given member in a sorted set"
+
+    #it "should add multiple sorted sets and store the resulting sorted set in a new key"
+
+    it "zrem should remove members add by zadd" do
+      @client.zadd("key1", 1, 3)
+      @client.zrem("key1", 3)
+      @client.zscore("key1", 3).should be_nil
+    end
+  end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..4826f7e
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,28 @@
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+require 'rspec'
+require 'fakeredis'
+require "fakeredis/rspec"
+
+require "support/shared_examples/sortable"
+
+RSpec.configure do |config|
+  # replaces -b -fdoc --color in .rspec
+  config.color = true
+  config.default_formatter = "doc"
+  config.backtrace_exclusion_patterns = []
+
+  config.mock_with :rspec do |c|
+    # TODO: upgrade should syntax to expect syntax
+    c.syntax = [:should, :expect]
+  end
+
+  config.expect_with :rspec do |c|
+    # TODO: upgrade should syntax to expect syntax
+    c.syntax = [:should, :expect]
+  end
+end
+
+def fakeredis?
+  true
+end
diff --git a/spec/spec_helper_live_redis.rb b/spec/spec_helper_live_redis.rb
new file mode 100644
index 0000000..19f6cc7
--- /dev/null
+++ b/spec/spec_helper_live_redis.rb
@@ -0,0 +1,14 @@
+require "spec_helper"
+
+# Remove memory so we test against actual redis
+Redis::Connection.drivers.pop
+
+RSpec.configure do |config|
+  config.before(:each) do
+    Redis.new.flushall
+  end
+end
+
+def fakeredis?
+  false
+end
diff --git a/spec/strings_spec.rb b/spec/strings_spec.rb
new file mode 100644
index 0000000..4b067e0
--- /dev/null
+++ b/spec/strings_spec.rb
@@ -0,0 +1,284 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+module FakeRedis
+  describe "StringsMethods" do
+
+    before(:each) do
+      @client = Redis.new
+    end
+
+    it "should append a value to key" do
+      @client.set("key1", "Hello")
+      @client.append("key1", " World")
+
+      @client.get("key1").should be == "Hello World"
+    end
+
+    it "should decrement the integer value of a key by one" do
+      @client.set("counter", "1")
+      @client.decr("counter")
+
+      @client.get("counter").should be == "0"
+    end
+
+    it "should decrement the integer value of a key by the given number" do
+      @client.set("counter", "10")
+      @client.decrby("counter", "5")
+
+      @client.get("counter").should be == "5"
+    end
+
+    it "should get the value of a key" do
+      @client.get("key2").should be == nil
+    end
+
+    it "should returns the bit value at offset in the string value stored at key" do
+      @client.set("key1", "a")
+
+      @client.getbit("key1", 1).should be == 1
+      @client.getbit("key1", 2).should be == 1
+      @client.getbit("key1", 3).should be == 0
+      @client.getbit("key1", 4).should be == 0
+      @client.getbit("key1", 5).should be == 0
+      @client.getbit("key1", 6).should be == 0
+      @client.getbit("key1", 7).should be == 1
+    end
+
+    it "should allow direct bit manipulation even if the string isn't set" do
+      @client.setbit("key1", 10, 1)
+      @client.getbit("key1", 10).should be == 1
+    end
+
+    context 'when a bit is previously set to 0' do
+      before { @client.setbit("key1", 10, 0) }
+
+      it 'setting it to 1 returns 0' do
+        expect(@client.setbit("key1", 10, 1)).to eql 0
+      end
+
+      it 'setting it to 0 returns 0' do
+        expect(@client.setbit("key1", 10, 0)).to eql 0
+      end
+    end
+
+    context 'when a bit is previously set to 1' do
+      before { @client.setbit("key1", 10, 1) }
+
+      it 'setting it to 0 returns 1' do
+        expect(@client.setbit("key1", 10, 0)).to eql 1
+      end
+
+      it 'setting it to 1 returns 1' do
+        expect(@client.setbit("key1", 10, 1)).to eql 1
+      end
+    end
+
+    it "should get a substring of the string stored at a key" do
+      @client.set("key1", "This a message")
+
+      @client.getrange("key1", 0, 3).should be == "This"
+      @client.substr("key1", 0, 3).should be == "This"
+    end
+
+    it "should set the string value of a key and return its old value" do
+      @client.set("key1","value1")
+
+      @client.getset("key1", "value2").should be == "value1"
+      @client.get("key1").should be == "value2"
+    end
+
+    it "should return nil for #getset if the key does not exist when setting" do
+      @client.getset("key1", "value1").should be == nil
+      @client.get("key1").should be == "value1"
+    end
+
+    it "should increment the integer value of a key by one" do
+      @client.set("counter", "1")
+      @client.incr("counter").should be == 2
+
+      @client.get("counter").should be == "2"
+    end
+
+    it "should not change the expire value of the key during incr" do
+      @client.set("counter", "1")
+      @client.expire("counter", 600).should be true
+      @client.ttl("counter").should be == 600
+      @client.incr("counter").should be == 2
+      @client.ttl("counter").should be == 600
+    end
+
+    it "should decrement the integer value of a key by one" do
+      @client.set("counter", "1")
+      @client.decr("counter").should be == 0
+
+      @client.get("counter").should be == "0"
+    end
+
+    it "should not change the expire value of the key during decr" do
+      @client.set("counter", "2")
+      @client.expire("counter", 600).should be true
+      @client.ttl("counter").should be == 600
+      @client.decr("counter").should be == 1
+      @client.ttl("counter").should be == 600
+    end
+
+    it "should increment the integer value of a key by the given number" do
+      @client.set("counter", "10")
+      @client.incrby("counter", "5").should be == 15
+      @client.incrby("counter", 2).should be == 17
+      @client.get("counter").should be == "17"
+    end
+
+    it "should not change the expire value of the key during incrby" do
+      @client.set("counter", "1")
+      @client.expire("counter", 600).should be true
+      @client.ttl("counter").should be == 600
+      @client.incrby("counter", "5").should be == 6
+      @client.ttl("counter").should be == 600
+    end
+
+    it "should decrement the integer value of a key by the given number" do
+      @client.set("counter", "10")
+      @client.decrby("counter", "5").should be == 5
+      @client.decrby("counter", 2).should be == 3
+      @client.get("counter").should be == "3"
+    end
+
+    it "should not change the expire value of the key during decrby" do
+      @client.set("counter", "8")
+      @client.expire("counter", 600).should be true
+      @client.ttl("counter").should be == 600
+      @client.decrby("counter", "3").should be == 5
+      @client.ttl("counter").should be == 600
+    end
+
+    it "should get the values of all the given keys" do
+      @client.set("key1", "value1")
+      @client.set("key2", "value2")
+      @client.set("key3", "value3")
+
+      @client.mget("key1", "key2", "key3").should be == ["value1", "value2", "value3"]
+      @client.mget(["key1", "key2", "key3"]).should be == ["value1", "value2", "value3"]
+    end
+
+    it "returns nil for non existent keys" do
+      @client.set("key1", "value1")
+      @client.set("key3", "value3")
+
+      @client.mget("key1", "key2", "key3", "key4").should be == ["value1", nil, "value3", nil]
+      @client.mget(["key1", "key2", "key3", "key4"]).should be == ["value1", nil, "value3", nil]
+    end
+
+    it 'raises an argument error when not passed any fields' do
+      @client.set("key3", "value3")
+
+      lambda { @client.mget }.should raise_error(Redis::CommandError)
+    end
+
+    it "should set multiple keys to multiple values" do
+      @client.mset(:key1, "value1", :key2, "value2")
+
+      @client.get("key1").should be == "value1"
+      @client.get("key2").should be == "value2"
+    end
+
+    it "should raise error if command arguments count is wrong" do
+      expect { @client.mset }.to raise_error(Redis::CommandError, "ERR wrong number of arguments for 'mset' command")
+      expect { @client.mset(:key1) }.to raise_error(Redis::CommandError, "ERR wrong number of arguments for 'mset' command")
+      expect { @client.mset(:key1, "value", :key2) }.to raise_error(Redis::CommandError, "ERR wrong number of arguments for MSET")
+
+      @client.get("key1").should be_nil
+      @client.get("key2").should be_nil
+    end
+
+    it "should set multiple keys to multiple values, only if none of the keys exist" do
+      @client.msetnx(:key1, "value1", :key2, "value2").should be == true
+      @client.msetnx(:key1, "value3", :key2, "value4").should be == false
+
+      @client.get("key1").should be == "value1"
+      @client.get("key2").should be == "value2"
+    end
+
+    it "should set multiple keys to multiple values with a hash" do
+      @client.mapped_mset(:key1 => "value1", :key2 => "value2")
+
+      @client.get("key1").should be == "value1"
+      @client.get("key2").should be == "value2"
+    end
+
+    it "should set multiple keys to multiple values with a hash, only if none of the keys exist" do
+      @client.mapped_msetnx(:key1 => "value1", :key2 => "value2").should be == true
+      @client.mapped_msetnx(:key1 => "value3", :key2 => "value4").should be == false
+
+      @client.get("key1").should be == "value1"
+      @client.get("key2").should be == "value2"
+    end
+
+    it "should set the string value of a key" do
+      @client.set("key1", "1")
+
+      @client.get("key1").should be == "1"
+    end
+
+    it "should sets or clears the bit at offset in the string value stored at key" do
+      @client.set("key1", "abc")
+      @client.setbit("key1", 11, 1)
+
+      @client.get("key1").should be == "arc"
+    end
+
+    it "should set the value and expiration of a key" do
+      @client.setex("key1", 30, "value1")
+
+      @client.get("key1").should be == "value1"
+      @client.ttl("key1").should be == 30
+    end
+
+    it "should set the value of a key, only if the key does not exist" do
+      @client.set("key1", "test value")
+      @client.setnx("key1", "new value")
+      @client.setnx("key2", "another value")
+
+      @client.get("key1").should be == "test value"
+      @client.get("key2").should be == "another value"
+    end
+
+    it "should overwrite part of a string at key starting at the specified offset" do
+      @client.set("key1", "Hello World")
+      @client.setrange("key1", 6, "Redis")
+
+      @client.get("key1").should be == "Hello Redis"
+    end
+
+    it "should get the length of the value stored in a key" do
+      @client.set("key1", "abc")
+
+      @client.strlen("key1").should be == 3
+    end
+
+    it "should return 0 bits when there's no key" do
+      @client.bitcount("key1").should be == 0
+    end
+
+    it "should count the number of bits of a string" do
+      @client.set("key1", "foobar")
+      @client.bitcount("key1").should be == 26
+    end
+
+    it "should count correctly with UTF-8 strings" do
+      @client.set("key1", '判')
+      @client.bitcount("key1").should be == 10
+    end
+
+    it "should count the number of bits of a string given a range" do
+      @client.set("key1", "foobar")
+
+      @client.bitcount("key1", 0, 0).should be == 4
+      @client.bitcount("key1", 1, 1).should be == 6
+      @client.bitcount("key1", 0, 1).should be == 10
+    end
+
+  end
+end
diff --git a/spec/support/shared_examples/sortable.rb b/spec/support/shared_examples/sortable.rb
new file mode 100644
index 0000000..c2e4e0e
--- /dev/null
+++ b/spec/support/shared_examples/sortable.rb
@@ -0,0 +1,69 @@
+shared_examples_for "a sortable" do
+  it 'returns empty array on nil' do
+    @client.sort(nil).should == []
+  end
+
+  context 'ordering' do
+    it 'orders ascending by default' do
+      @client.sort(@key).should == ['1', '2']
+    end
+
+    it 'orders by ascending when specified' do
+      @client.sort(@key, :order => "ASC").should == ['1', '2']
+    end
+
+    it 'orders by descending when specified' do
+      @client.sort(@key, :order => "DESC").should == ['2', '1']
+    end
+
+    it "orders by ascending when alpha is specified" do
+      @client.sort(@key, :order => "ALPHA").should == ["1", "2"]
+    end
+  end
+
+  context 'projections' do
+    it 'projects element when :get is "#"' do
+      @client.sort(@key, :get => '#').should == ['1', '2']
+    end
+
+    it 'projects through a key pattern' do
+      @client.sort(@key, :get => 'fake-redis-test:values_*').should == ['a', 'b']
+    end
+
+    it 'projects through a key pattern and reflects element' do
+      @client.sort(@key, :get => ['#', 'fake-redis-test:values_*']).should == [['1', 'a'], ['2', 'b']]
+    end
+
+    it 'projects through a hash key pattern' do
+      @client.sort(@key, :get => 'fake-redis-test:hash_*->key').should == ['x', 'y']
+    end
+  end
+
+  context 'weights' do
+    it 'weights by projecting through a key pattern' do
+      @client.sort(@key, :by => "fake-redis-test:weight_*").should == ['2', '1']
+    end
+
+    it 'weights by projecting through a key pattern and a specific order' do
+      @client.sort(@key, :order => "DESC", :by => "fake-redis-test:weight_*").should == ['1', '2']
+    end
+  end
+
+  context 'limit' do
+    it 'only returns requested window in the enumerable' do
+      @client.sort(@key, :limit => [0, 1]).should == ['1']
+    end
+  end
+
+  context 'store' do
+    it 'stores into another key' do
+      @client.sort(@key, :store => "fake-redis-test:some_bucket").should == 2
+      @client.lrange("fake-redis-test:some_bucket", 0, -1).should == ['1', '2']
+    end
+
+    it "stores into another key with other options specified" do
+      @client.sort(@key, :store => "fake-redis-test:some_bucket", :by => "fake-redis-test:weight_*").should == 2
+      @client.lrange("fake-redis-test:some_bucket", 0, -1).should == ['2', '1']
+    end
+  end
+end
diff --git a/spec/transactions_spec.rb b/spec/transactions_spec.rb
new file mode 100644
index 0000000..f93c207
--- /dev/null
+++ b/spec/transactions_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "TransactionsMethods" do
+    before(:all) do
+      @client = Redis.new
+    end
+
+    before(:each) do
+      @client.discard rescue nil
+    end
+
+    context '#multi' do
+      it "should respond with 'OK'" do
+        @client.multi.should == 'OK'
+      end
+
+      it "should forbid nesting" do
+        @client.multi
+        lambda{@client.multi}.should raise_error(Redis::CommandError)
+      end
+
+      it "should mark the start of a transaction block" do
+        transaction = @client.multi do |multi|
+          multi.set("key1", "1")
+          multi.set("key2", "2")
+          multi.expire("key1", 123)
+          multi.mget("key1", "key2")
+        end
+
+        transaction.should be == ["OK", "OK", true, ["1", "2"]]
+      end
+    end
+
+    context '#discard' do
+      it "should responde with 'OK' after #multi" do
+        @client.multi
+        @client.discard.should == 'OK'
+      end
+
+      it "can't be run outside of #multi/#exec" do
+        lambda{@client.discard}.should raise_error(Redis::CommandError)
+      end
+    end
+
+    context '#exec' do
+      it "can't be run outside of #multi" do
+        lambda{@client.exec}.should raise_error(Redis::CommandError)
+      end
+    end
+
+    context 'saving up commands for later' do
+      before(:each) do
+        @client.multi
+        @string = 'fake-redis-test:string'
+        @list = 'fake-redis-test:list'
+      end
+
+      it "makes commands respond with 'QUEUED'" do
+        @client.set(@string, 'string').should == 'QUEUED'
+        @client.lpush(@list, 'list').should == 'QUEUED'
+      end
+
+      it "gives you the commands' responses when you call #exec" do
+        @client.set(@string, 'string')
+        @client.lpush(@list, 'list')
+        @client.lpush(@list, 'list')
+
+        @client.exec.should == ['OK', 1, 2]
+      end
+
+      it "does not raise exceptions, but rather puts them in #exec's response" do
+        @client.set(@string, 'string')
+        @client.lpush(@string, 'oops!')
+        @client.lpush(@list, 'list')
+
+        responses = @client.exec
+        responses[0].should == 'OK'
+        responses[1].should be_a(RuntimeError)
+        responses[2].should == 1
+      end
+    end
+  end
+end
diff --git a/spec/upcase_method_name_spec.rb b/spec/upcase_method_name_spec.rb
new file mode 100644
index 0000000..f0272e8
--- /dev/null
+++ b/spec/upcase_method_name_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+module FakeRedis
+  describe "UPCASE method name will call downcase method" do
+
+    before do
+      @client = Redis.new
+    end
+
+    it "#ZCOUNT" do
+      @client.ZCOUNT("key", 2, 3).should == @client.zcount("key", 2, 3)
+    end
+
+    it "#ZSCORE" do
+      @client.ZSCORE("key", 2).should == @client.zscore("key", 2)
+    end
+  end
+end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-fakeredis.git



More information about the Pkg-ruby-extras-commits mailing list